@hydranium/glsp-server 1.0.0-next.12 → 1.0.0-next.127
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +20 -5
- package/lib/browser/start-glsp-server-in-worker.d.ts +30 -39
- package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -1
- package/lib/browser/start-glsp-server-in-worker.js +43 -8
- package/lib/browser/start-glsp-server-in-worker.js.map +1 -1
- package/lib/command/hydranium-glsp-command-stack.d.ts +27 -0
- package/lib/command/hydranium-glsp-command-stack.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-command-stack.js +53 -0
- package/lib/command/hydranium-glsp-command-stack.js.map +1 -0
- package/lib/command/hydranium-glsp-recording-command.d.ts +22 -22
- package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -1
- package/lib/command/hydranium-glsp-recording-command.js +25 -26
- package/lib/command/hydranium-glsp-recording-command.js.map +1 -1
- package/lib/command/index.d.ts +1 -0
- package/lib/command/index.d.ts.map +1 -1
- package/lib/command/index.js +1 -0
- package/lib/command/index.js.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +12 -3
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -1
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +18 -2
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -1
- package/lib/launcher/hydranium-glsp-server.d.ts +10 -6
- package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
- package/lib/launcher/hydranium-glsp-server.js +16 -6
- package/lib/launcher/hydranium-glsp-server.js.map +1 -1
- package/lib/messages/index.d.ts +24 -0
- package/lib/messages/index.d.ts.map +1 -0
- package/lib/messages/index.js +24 -0
- package/lib/messages/index.js.map +1 -0
- package/lib/node/start-glsp-server.d.ts +1 -1
- package/lib/node/start-glsp-server.d.ts.map +1 -1
- package/lib/node/start-glsp-server.js +2 -2
- package/lib/node/start-glsp-server.js.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.d.ts +110 -43
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.js +139 -60
- package/lib/state/abstract-hydranium-glsp-state.js.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.d.ts +6 -6
- package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/full-text-hydranium-glsp-state.js +6 -10
- package/lib/state/full-text-hydranium-glsp-state.js.map +1 -1
- package/lib/state/reconcile-source-model-write.d.ts +9 -8
- package/lib/state/reconcile-source-model-write.d.ts.map +1 -1
- package/lib/state/reconcile-source-model-write.js +8 -8
- package/lib/state/reconcile-source-model-write.js.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.d.ts +59 -59
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-multi-document-glsp-state.js +84 -82
- package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +19 -18
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js +26 -30
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -1
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts +30 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.d.ts.map +1 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.js +47 -0
- package/lib/storage/hydranium-glsp-request-save-model-action-handler.js.map +1 -0
- package/lib/storage/hydranium-glsp-storage.d.ts +295 -79
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
- package/lib/storage/hydranium-glsp-storage.js +435 -94
- package/lib/storage/hydranium-glsp-storage.js.map +1 -1
- package/lib/storage/index.d.ts +2 -1
- package/lib/storage/index.d.ts.map +1 -1
- package/lib/storage/index.js +2 -1
- package/lib/storage/index.js.map +1 -1
- package/lib/storage/save-delivery-policy.d.ts +46 -0
- package/lib/storage/save-delivery-policy.d.ts.map +1 -0
- package/lib/storage/{save-conflict-policy.js → save-delivery-policy.js} +7 -8
- package/lib/storage/save-delivery-policy.js.map +1 -0
- package/lib/validation/diagnostic-markers.d.ts +2 -2
- package/lib/validation/diagnostic-markers.d.ts.map +1 -1
- package/lib/validation/diagnostic-markers.js +7 -0
- package/lib/validation/diagnostic-markers.js.map +1 -1
- package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -1
- package/lib/validation/hydranium-glsp-model-validator.js +1 -1
- package/lib/validation/hydranium-glsp-model-validator.js.map +1 -1
- package/package.json +17 -8
- package/src/browser/start-glsp-server-in-worker.ts +41 -44
- package/src/command/hydranium-glsp-command-stack.ts +36 -0
- package/src/command/hydranium-glsp-recording-command.ts +26 -26
- package/src/command/index.ts +1 -0
- package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +27 -3
- package/src/launcher/hydranium-glsp-server.ts +16 -6
- package/src/messages/index.ts +30 -0
- package/src/node/start-glsp-server.ts +2 -3
- package/src/state/abstract-hydranium-glsp-state.ts +174 -73
- package/src/state/full-text-hydranium-glsp-state.ts +7 -11
- package/src/state/reconcile-source-model-write.ts +18 -15
- package/src/state/reconciling-multi-document-glsp-state.ts +86 -83
- package/src/state/reconciling-transfer-hydranium-glsp-state.ts +27 -31
- package/src/storage/hydranium-glsp-request-save-model-action-handler.ts +40 -0
- package/src/storage/hydranium-glsp-storage.ts +497 -103
- package/src/storage/index.ts +2 -1
- package/src/storage/save-delivery-policy.ts +44 -0
- package/src/validation/diagnostic-markers.ts +10 -3
- package/src/validation/hydranium-glsp-model-validator.ts +3 -3
- package/lib/storage/save-conflict-policy.d.ts +0 -56
- package/lib/storage/save-conflict-policy.d.ts.map +0 -1
- package/lib/storage/save-conflict-policy.js.map +0 -1
- package/src/storage/save-conflict-policy.ts +0 -52
|
@@ -7,11 +7,19 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import {
|
|
10
|
+
import {
|
|
11
|
+
type ActionHandlerConstructor,
|
|
12
|
+
type BindingTarget,
|
|
13
|
+
type CommandStack,
|
|
14
|
+
DiagramModule,
|
|
15
|
+
type InstanceMultiBinding
|
|
16
|
+
} from '@eclipse-glsp/server';
|
|
11
17
|
import { injectable, type interfaces } from 'inversify';
|
|
12
18
|
import type { LanguageMetaData } from '@hydranium/langium';
|
|
13
19
|
import { type ServerLanguageServices, type ServerSharedServices, typedMetadata } from '@hydranium/core';
|
|
14
20
|
import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
|
|
21
|
+
import { HydraniumGlspRequestSaveModelActionHandler } from '../storage/hydranium-glsp-request-save-model-action-handler.js';
|
|
22
|
+
import { HydraniumGlspCommandStack } from '../command/hydranium-glsp-command-stack.js';
|
|
15
23
|
|
|
16
24
|
/**
|
|
17
25
|
* Bind {@link HydraniumTypes}.DiagramLanguage on a GLSP **session** container
|
|
@@ -20,8 +28,10 @@ import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
|
|
|
20
28
|
* Exported separately from {@link AbstractHydraniumGlspDiagramModule} for adopters
|
|
21
29
|
* whose diagram module already extends an intermediate base of their own and
|
|
22
30
|
* cannot take the framework base class; call it from `configure` after
|
|
23
|
-
* `super.configure(...)
|
|
24
|
-
*
|
|
31
|
+
* `super.configure(...)`, add {@link HydraniumGlspRequestSaveModelActionHandler}
|
|
32
|
+
* in `configureActionHandlers`, and return {@link HydraniumGlspCommandStack} from
|
|
33
|
+
* `bindCommandStack`, all of which the base also does. The base class is the
|
|
34
|
+
* preferred entry point because it makes the declaration non-optional.
|
|
25
35
|
*
|
|
26
36
|
* **Why the session tier.** GLSP builds its app container once per process,
|
|
27
37
|
* before any document exists, so a per-language service bound there can only
|
|
@@ -114,4 +124,18 @@ export abstract class AbstractHydraniumGlspDiagramModule extends DiagramModule {
|
|
|
114
124
|
super.configure(bind, unbind, isBound, rebind);
|
|
115
125
|
bindDiagramLanguage(bind, this.declareLanguage());
|
|
116
126
|
}
|
|
127
|
+
|
|
128
|
+
/** {@link HydraniumGlspCommandStack}, so the diagram is dirty exactly while a document it has open is. */
|
|
129
|
+
protected override bindCommandStack(): BindingTarget<CommandStack> {
|
|
130
|
+
return HydraniumGlspCommandStack;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Adds {@link HydraniumGlspRequestSaveModelActionHandler}, the save the
|
|
135
|
+
* Theia client sends, beside GLSP's own save handler.
|
|
136
|
+
*/
|
|
137
|
+
protected override configureActionHandlers(binding: InstanceMultiBinding<ActionHandlerConstructor>): void {
|
|
138
|
+
super.configureActionHandlers(binding);
|
|
139
|
+
binding.add(HydraniumGlspRequestSaveModelActionHandler);
|
|
140
|
+
}
|
|
117
141
|
}
|
|
@@ -15,6 +15,7 @@ import {
|
|
|
15
15
|
type RequestAction,
|
|
16
16
|
type ResponseAction
|
|
17
17
|
} from '@eclipse-glsp/server';
|
|
18
|
+
import { RequestSaveModelAction } from '@hydranium/protocol';
|
|
18
19
|
import { injectable } from 'inversify';
|
|
19
20
|
|
|
20
21
|
/**
|
|
@@ -26,9 +27,10 @@ import { injectable } from 'inversify';
|
|
|
26
27
|
* `error.cause?.toString()` whenever the error is a {@link GLSPServerError},
|
|
27
28
|
* and that one value feeds both the server log and the client's
|
|
28
29
|
* {@link RejectAction}. A {@link GLSPServerError} carrying no `cause` therefore
|
|
29
|
-
* reaches
|
|
30
|
-
* while a plain `Error` survives,
|
|
31
|
-
* itself. The typed error is the trap,
|
|
30
|
+
* reaches its readers as `undefined` — the log line and the client's
|
|
31
|
+
* action-dispatcher warning both print it — while a plain `Error` survives,
|
|
32
|
+
* because the other branch reads the error itself. The typed error is the trap,
|
|
33
|
+
* and it is the one upstream's own
|
|
32
34
|
* `getOrThrow` helper raises: that helper's signature cannot pass a `cause`, so
|
|
33
35
|
* an adopter using the sanctioned helper has no fix available on their side.
|
|
34
36
|
* Restating the cause at each throw site is the alternative, and it costs
|
|
@@ -63,9 +65,12 @@ export class HydraniumGlspServer extends DefaultGLSPServer {
|
|
|
63
65
|
/**
|
|
64
66
|
* Lifted verbatim from `@eclipse-glsp/server` 2.7.0's
|
|
65
67
|
* {@link DefaultGLSPServer.handleClientRequest}, with the detail computation
|
|
66
|
-
* delegated to {@link requestFailureDetail}
|
|
67
|
-
*
|
|
68
|
-
*
|
|
68
|
+
* delegated to {@link requestFailureDetail}, and one addition: a failed
|
|
69
|
+
* {@link RequestSaveModelAction} also raises the error notification a failed
|
|
70
|
+
* `SaveModelAction` raises. A rejection reaches only the client's log, and a
|
|
71
|
+
* Theia host only logs a rejected save, so without it a save that failed
|
|
72
|
+
* would tell the user nothing. Nothing else here — the timeout branch, the
|
|
73
|
+
* response dispatch, the nested send-failure handling — relates to either.
|
|
69
74
|
*
|
|
70
75
|
* **This body does not track upstream.** A release that changes request
|
|
71
76
|
* handling would silently not apply, on the path every request takes. There
|
|
@@ -96,6 +101,11 @@ export class HydraniumGlspServer extends DefaultGLSPServer {
|
|
|
96
101
|
detail
|
|
97
102
|
});
|
|
98
103
|
this.sendResponseToClient(clientId, reject);
|
|
104
|
+
// Inside the guard: `process` does not await this method, so a
|
|
105
|
+
// closed connection throwing here would be an unhandled rejection.
|
|
106
|
+
if (action.kind === RequestSaveModelAction.KIND) {
|
|
107
|
+
this.handleProcessError({ clientId, action }, error);
|
|
108
|
+
}
|
|
99
109
|
} catch (sendError: unknown) {
|
|
100
110
|
this.logger.error(`Failed to send rejection for request '${action.requestId}':`, sendError);
|
|
101
111
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
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
|
+
* Every user-facing message `@hydranium/glsp-server` raises.
|
|
12
|
+
*
|
|
13
|
+
* **Each one is rendered AT ITS RAISE SITE**, through the shared
|
|
14
|
+
* `MessageRenderer`, because GLSP's action protocol carries no slot for an
|
|
15
|
+
* identity on any of its actions — so the identity is gone the moment a raise
|
|
16
|
+
* site formats its text, and there is nothing at the error boundary to render
|
|
17
|
+
* from. That makes this head the one place where a new message can ship
|
|
18
|
+
* unrendered without any single chokepoint noticing, which is why the package's
|
|
19
|
+
* own test suite scans for a declaration reached by `.format()` instead.
|
|
20
|
+
*
|
|
21
|
+
* The barrel is also how an adopter rendering their own diagram chrome
|
|
22
|
+
* discovers the codes exist.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
export {
|
|
26
|
+
DIAGRAM_READONLY_PARSE_ERROR,
|
|
27
|
+
DIAGRAM_SESSION_REFUSED,
|
|
28
|
+
SAVE_TARGET_UNKNOWN,
|
|
29
|
+
SOURCE_URI_MISSING
|
|
30
|
+
} from '../storage/hydranium-glsp-storage.js';
|
|
@@ -16,8 +16,7 @@ import {
|
|
|
16
16
|
defaultSocketLaunchOptions
|
|
17
17
|
} from '@eclipse-glsp/server/node.js';
|
|
18
18
|
import { Container, type ContainerModule } from 'inversify';
|
|
19
|
-
import type
|
|
20
|
-
import { Deferred } from '@hydranium/langium';
|
|
19
|
+
import { Deferred, type Logger } from '@hydranium/protocol';
|
|
21
20
|
import type * as net from 'node:net';
|
|
22
21
|
import type { IntegratedServer } from '@hydranium/core';
|
|
23
22
|
import { createGlspFrameworkOverrides } from '../launcher/glsp-framework-overrides.js';
|
|
@@ -177,7 +176,7 @@ export function startGlspServer(options: GlspServerOptions): StartedGlspServer {
|
|
|
177
176
|
if (options.lspConnection && options.portCommand) {
|
|
178
177
|
options.lspConnection.onRequest(options.portCommand, () => portHandle.port);
|
|
179
178
|
}
|
|
180
|
-
lifecycle?.info(`[GlspServer] Ready to accept new client requests on port: ${address.port}`);
|
|
179
|
+
lifecycle?.info(`[GlspServer] Ready to accept new client requests on ${address.address} port: ${address.port}`);
|
|
181
180
|
started.resolve();
|
|
182
181
|
} else {
|
|
183
182
|
const message = address === null ? 'address is null' : `bound to non-TCP "${String(address)}"`;
|
|
@@ -12,6 +12,7 @@ import { Emitter, type Event } from 'vscode-jsonrpc';
|
|
|
12
12
|
import { inject, injectable, optional } from 'inversify';
|
|
13
13
|
import { type AstNode, DocumentState, URI } from '@hydranium/langium';
|
|
14
14
|
import {
|
|
15
|
+
type ClientSession,
|
|
15
16
|
type HydraniumScopeProvider,
|
|
16
17
|
type LanguageTarget,
|
|
17
18
|
type NameProvider,
|
|
@@ -19,7 +20,16 @@ import {
|
|
|
19
20
|
type ServerLanguageServices,
|
|
20
21
|
type ServerSharedServices
|
|
21
22
|
} from '@hydranium/core';
|
|
22
|
-
import
|
|
23
|
+
import {
|
|
24
|
+
type BasedOn,
|
|
25
|
+
type SnapshotVersion,
|
|
26
|
+
type ConflictResolver,
|
|
27
|
+
type Logger,
|
|
28
|
+
type Tracer,
|
|
29
|
+
asSnapshotVersion,
|
|
30
|
+
NO_MATCHING_VERSION,
|
|
31
|
+
TIMED_OUT
|
|
32
|
+
} from '@hydranium/protocol';
|
|
23
33
|
import { type HydraniumGlspIndex } from './hydranium-glsp-index.js';
|
|
24
34
|
import { HydraniumTypes } from './hydranium-shared-core-services.js';
|
|
25
35
|
import { ModelReadyTimeoutError } from './model-ready-timeout-error.js';
|
|
@@ -87,12 +97,38 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
87
97
|
/** Narrows {@link DefaultModelState.index} to the framework-extended index type. */
|
|
88
98
|
declare readonly index: HydraniumGlspIndex;
|
|
89
99
|
|
|
100
|
+
/**
|
|
101
|
+
* The client session this diagram works as, registered under `clientId` by
|
|
102
|
+
* the source-model storage and ended with the GLSP client session.
|
|
103
|
+
* `undefined` for GLSP's placeholder client, and when the id was held by
|
|
104
|
+
* another participant, in which case the diagram does not load.
|
|
105
|
+
*
|
|
106
|
+
* Every write of the diagram goes through this session, which writes only a
|
|
107
|
+
* document it has open and opens nothing. A one-shot write to a document
|
|
108
|
+
* outside the write set goes through
|
|
109
|
+
* {@link ClientSession.withOpen}, and a document the diagram brings into
|
|
110
|
+
* existence through {@link ClientSession.create}.
|
|
111
|
+
*/
|
|
112
|
+
modelSession?: ClientSession<AstNode>;
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* {@link modelSession}, or a throw without one: the diagram never loaded or
|
|
116
|
+
* has ended. Looking `clientId` up through `ModelService.getSession` instead
|
|
117
|
+
* would write as whichever participant holds that id.
|
|
118
|
+
*/
|
|
119
|
+
protected requireModelSession(): ClientSession<AstNode> {
|
|
120
|
+
if (!this.modelSession) {
|
|
121
|
+
throw new Error(`No client session is registered for ${this.clientId}; the diagram cannot write`);
|
|
122
|
+
}
|
|
123
|
+
return this.modelSession;
|
|
124
|
+
}
|
|
125
|
+
|
|
90
126
|
protected _sourceUri!: string;
|
|
91
127
|
protected _sourceRoot!: TRoot;
|
|
92
128
|
protected _tracer?: Tracer;
|
|
93
|
-
protected
|
|
94
|
-
/**
|
|
95
|
-
protected readonly _secondaryVersions = new Map<string,
|
|
129
|
+
protected _basedOn: SnapshotVersion = asSnapshotVersion(0);
|
|
130
|
+
/** Snapshot versions of the secondary write set, keyed by URI. See {@link trackSecondaryDocument}. */
|
|
131
|
+
protected readonly _secondaryVersions = new Map<string, SnapshotVersion>();
|
|
96
132
|
|
|
97
133
|
/** Backing emitter for {@link onSecondaryUrisChanged}; fired only on a membership change. */
|
|
98
134
|
protected readonly secondaryUrisChangedEmitter = new Emitter<void>();
|
|
@@ -115,7 +151,7 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
115
151
|
setSourceRoot(uri: string, root: TRoot): void {
|
|
116
152
|
this._sourceUri = uri;
|
|
117
153
|
this._sourceRoot = root;
|
|
118
|
-
this.
|
|
154
|
+
this._basedOn = this.readSnapshotVersion(uri);
|
|
119
155
|
// Re-read the secondary write set for the same reason the primary is
|
|
120
156
|
// re-read: the write that brought us here advanced their versions too, and
|
|
121
157
|
// a stale based-on version would gate the NEXT command against a
|
|
@@ -124,7 +160,7 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
124
160
|
this.refreshSecondaryVersions();
|
|
125
161
|
this.set(SOURCE_URI_ARG, uri);
|
|
126
162
|
this._tracer = this.baseTracer.withUri(uri);
|
|
127
|
-
this.tracer.debug(`Captured source root at doc.version=v${this.
|
|
163
|
+
this.tracer.debug(`Captured source root at doc.version=v${this._basedOn}`);
|
|
128
164
|
this.checkDeclaredLanguage(uri);
|
|
129
165
|
this.index.indexSourceRoot(root, uri);
|
|
130
166
|
}
|
|
@@ -241,25 +277,36 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
241
277
|
}
|
|
242
278
|
|
|
243
279
|
/**
|
|
244
|
-
*
|
|
245
|
-
*
|
|
246
|
-
* at that point in time — frozen until the next `setSourceRoot`, then
|
|
247
|
-
* re-read from `LangiumDocuments`.
|
|
280
|
+
* Snapshot version taken at the last {@link setSourceRoot} call — frozen
|
|
281
|
+
* until the next one, then re-read from the document store.
|
|
248
282
|
*
|
|
249
283
|
* Threaded by `HydraniumGlspRecordingCommand` into
|
|
250
|
-
* {@link updateSourceModel}
|
|
251
|
-
*
|
|
252
|
-
* conflict gate against the captured based-on version. See
|
|
284
|
+
* {@link updateSourceModel} so the downstream session `update` / `save`
|
|
285
|
+
* gates against what the command was authored on. See
|
|
253
286
|
* `@hydranium/protocol#ConflictError` for the detection contract.
|
|
254
287
|
*
|
|
288
|
+
* **This, not a number read at write time, is what a write must be gated
|
|
289
|
+
* on.** A version read when the write is issued is the store's current one,
|
|
290
|
+
* so it matches by construction and the gate can never fire.
|
|
291
|
+
*/
|
|
292
|
+
get basedOn(): SnapshotVersion {
|
|
293
|
+
return this._basedOn;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Text-document version of {@link basedOn}. Mirrors the server-side
|
|
298
|
+
* `TextDocuments.version(uri)` counter as of the snapshot, or is
|
|
299
|
+
* `NO_MATCHING_VERSION` when the store's text could not be tied to the
|
|
300
|
+
* root (see {@link readSnapshotVersion}).
|
|
301
|
+
*
|
|
255
302
|
* Falls back to `0` when the document is absent from `LangiumDocuments`
|
|
256
|
-
*
|
|
257
|
-
* that was never opened. Callers that need to distinguish
|
|
303
|
+
* when the snapshot is taken — the same observable state callers see for a
|
|
304
|
+
* doc that was never opened. Callers that need to distinguish
|
|
258
305
|
* "never-versioned" from "v0" must consult the document registry
|
|
259
306
|
* directly.
|
|
260
307
|
*/
|
|
261
308
|
get version(): number {
|
|
262
|
-
return this.
|
|
309
|
+
return this._basedOn;
|
|
263
310
|
}
|
|
264
311
|
|
|
265
312
|
// ============================================================
|
|
@@ -267,8 +314,8 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
267
314
|
// ============================================================
|
|
268
315
|
|
|
269
316
|
/**
|
|
270
|
-
* Register `uri` as a document this state also writes,
|
|
271
|
-
* text-document version.
|
|
317
|
+
* Register `uri` as a document this state also writes, snapshotting its
|
|
318
|
+
* current text-document version.
|
|
272
319
|
*
|
|
273
320
|
* A diagram whose layout lives in its own file, or whose canvas creates
|
|
274
321
|
* elements in a referenced semantic document, writes more than the document
|
|
@@ -281,7 +328,7 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
281
328
|
* identified once the operation names the node. What the framework supplies
|
|
282
329
|
* is the part an adopter cannot reconstruct after the fact — the version each
|
|
283
330
|
* document was at BEFORE the command mutated anything (see
|
|
284
|
-
* {@link
|
|
331
|
+
* {@link snapshotVersionOf}). Idempotent; registering the primary is ignored,
|
|
285
332
|
* since {@link version} already tracks it. Registering a URI not already in
|
|
286
333
|
* the set fires {@link onSecondaryUrisChanged}.
|
|
287
334
|
*/
|
|
@@ -290,7 +337,7 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
290
337
|
return;
|
|
291
338
|
}
|
|
292
339
|
const added = !this._secondaryVersions.has(uri);
|
|
293
|
-
this._secondaryVersions.set(uri, this.
|
|
340
|
+
this._secondaryVersions.set(uri, this.readSnapshotVersion(uri));
|
|
294
341
|
if (added) {
|
|
295
342
|
this.secondaryUrisChangedEmitter.fire();
|
|
296
343
|
}
|
|
@@ -310,14 +357,15 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
310
357
|
|
|
311
358
|
/**
|
|
312
359
|
* Fires whenever the secondary write set gains or loses a URI — never when a
|
|
313
|
-
* tracked document's
|
|
360
|
+
* tracked document's snapshot version is merely refreshed, which happens on
|
|
314
361
|
* every {@link setSourceRoot} and changes nothing a subscriber cares about.
|
|
315
362
|
*
|
|
316
363
|
* **This exists so no caller has to know when the set can change.** The set is
|
|
317
364
|
* usually discovered rather than known, and the discovery point is an
|
|
318
|
-
* operation handler naming a node — not a
|
|
319
|
-
* re-derived the set after each
|
|
320
|
-
* it silently: the document would simply never be
|
|
365
|
+
* operation handler naming a node — not a read of the source root. A
|
|
366
|
+
* subscriber that instead re-derived the set after each such read would miss
|
|
367
|
+
* exactly that case, and miss it silently: the document would simply never be
|
|
368
|
+
* watched, with nothing logged.
|
|
321
369
|
* Announcing the change here moves the obligation to the one place that can
|
|
322
370
|
* always discharge it.
|
|
323
371
|
*
|
|
@@ -336,37 +384,69 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
336
384
|
}
|
|
337
385
|
|
|
338
386
|
/**
|
|
339
|
-
*
|
|
340
|
-
*
|
|
341
|
-
*
|
|
387
|
+
* Snapshot version for `uri` — {@link basedOn} for the primary, the version
|
|
388
|
+
* taken at {@link trackSecondaryDocument} (refreshed by {@link setSourceRoot})
|
|
389
|
+
* for a secondary, `undefined` for anything untracked.
|
|
342
390
|
*
|
|
343
|
-
* `undefined` rather than
|
|
391
|
+
* `undefined` rather than v0 for an untracked URI deliberately: `0` is a real
|
|
344
392
|
* version meaning "present but never edited", so collapsing the two would let
|
|
345
|
-
* a caller gate a write against a document
|
|
393
|
+
* a caller gate a write against a document this state never read.
|
|
346
394
|
*/
|
|
347
|
-
|
|
395
|
+
snapshotVersionOf(uri: string): SnapshotVersion | undefined {
|
|
348
396
|
if (uri === this._sourceUri) {
|
|
349
|
-
return this.
|
|
397
|
+
return this._basedOn;
|
|
350
398
|
}
|
|
351
399
|
return this._secondaryVersions.get(uri);
|
|
352
400
|
}
|
|
353
401
|
|
|
354
402
|
/**
|
|
355
|
-
* Read a document's
|
|
356
|
-
* canonicalizing gateway
|
|
357
|
-
* the document keyed by its real path rather than
|
|
358
|
-
* would silently weaken the conflict gate).
|
|
359
|
-
*
|
|
360
|
-
*
|
|
403
|
+
* Read a document's version out of the live store and mark it as a snapshot,
|
|
404
|
+
* through the model service's canonicalizing gateway so a URI spelled through
|
|
405
|
+
* a symlink still resolves to the document keyed by its real path rather than
|
|
406
|
+
* stranding at `0` (which would silently weaken the conflict gate).
|
|
407
|
+
*
|
|
408
|
+
* **Call this only at the point the source root is read.** It is the one
|
|
409
|
+
* place the framework turns a live version into a snapshot one, and the value
|
|
410
|
+
* is true only because that is where it is called. Called at write time it
|
|
411
|
+
* answers with the version the write is about to be compared against, so the
|
|
412
|
+
* gate passes unconditionally.
|
|
413
|
+
*
|
|
414
|
+
* Falls back to v0 for a URI neither store knows, matching what
|
|
415
|
+
* {@link version} reports for a document that was never opened.
|
|
416
|
+
*
|
|
417
|
+
* **The version of the text store, which the gate compares against.**
|
|
418
|
+
* When the store holds the text the root was parsed from, that is its
|
|
419
|
+
* version, even while the built document still carries an older number.
|
|
420
|
+
* When the store holds other text, `NO_MATCHING_VERSION`, so the write
|
|
421
|
+
* conflicts instead of overwriting text the root never saw: the
|
|
422
|
+
* reconciling states reconcile it, `FullTextHydraniumGlspState` throws the
|
|
423
|
+
* `ConflictError`. When the root keeps no syntax tree to read its text
|
|
424
|
+
* from, the built document's text stands in for it, unless the built
|
|
425
|
+
* document IS the store's, whose text says nothing about what was parsed;
|
|
426
|
+
* that also gets `NO_MATCHING_VERSION`.
|
|
361
427
|
*/
|
|
362
|
-
protected
|
|
363
|
-
|
|
428
|
+
protected readSnapshotVersion(uri: string): SnapshotVersion {
|
|
429
|
+
const document = this.sharedServices.model.ModelService.getDocument(uri);
|
|
430
|
+
const stored = document && this.sharedServices.workspace.TextDocuments.get(uri);
|
|
431
|
+
if (stored) {
|
|
432
|
+
const parsed = document.parseResult.value.$cstNode?.root.fullText;
|
|
433
|
+
if (parsed !== undefined) {
|
|
434
|
+
return parsed === stored.getText() ? asSnapshotVersion(stored.version) : NO_MATCHING_VERSION;
|
|
435
|
+
}
|
|
436
|
+
if (document.textDocument === stored) {
|
|
437
|
+
return NO_MATCHING_VERSION;
|
|
438
|
+
}
|
|
439
|
+
if (stored.version !== document.textDocument.version && stored.getText() === document.textDocument.getText()) {
|
|
440
|
+
return asSnapshotVersion(stored.version);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return this.sharedServices.model.ModelService.snapshot(uri)?.version ?? asSnapshotVersion(0);
|
|
364
444
|
}
|
|
365
445
|
|
|
366
|
-
/** Re-
|
|
446
|
+
/** Re-take every registered secondary's snapshot version from the document store. */
|
|
367
447
|
protected refreshSecondaryVersions(): void {
|
|
368
448
|
for (const uri of [...this._secondaryVersions.keys()]) {
|
|
369
|
-
this._secondaryVersions.set(uri, this.
|
|
449
|
+
this._secondaryVersions.set(uri, this.readSnapshotVersion(uri));
|
|
370
450
|
}
|
|
371
451
|
}
|
|
372
452
|
|
|
@@ -392,6 +472,28 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
392
472
|
}
|
|
393
473
|
}
|
|
394
474
|
|
|
475
|
+
/**
|
|
476
|
+
* The root that `uri`'s text parses to (the text store's, or the built
|
|
477
|
+
* document's when the store holds none), read once the document has
|
|
478
|
+
* validated; `undefined` when it cannot be read.
|
|
479
|
+
*
|
|
480
|
+
* **Parsed afresh, never the built root.** Operation handlers edit the
|
|
481
|
+
* built root in place, so until a rebuild replaces it, it already holds
|
|
482
|
+
* the edit being reconciled, and replaying the edit onto it applies it
|
|
483
|
+
* twice. The root is parsed, not linked: an encoder hook reading a
|
|
484
|
+
* reference's `ref` sees `undefined` on it.
|
|
485
|
+
*/
|
|
486
|
+
protected async readCurrentRoot(uri: string): Promise<AstNode | undefined> {
|
|
487
|
+
const modelService = this.sharedServices.model.ModelService;
|
|
488
|
+
if (!(await modelService.validated(uri).catch(() => undefined))) {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
const text = this.sharedServices.workspace.TextDocuments.get(uri)?.getText() ?? modelService.getDocument(uri)?.textDocument.getText();
|
|
492
|
+
return text === undefined
|
|
493
|
+
? undefined
|
|
494
|
+
: this.sharedServices.workspace.LangiumDocumentFactory.fromString(text, URI.parse(uri)).parseResult.value;
|
|
495
|
+
}
|
|
496
|
+
|
|
395
497
|
/**
|
|
396
498
|
* Wait until the captured document reaches `state`, then run
|
|
397
499
|
* {@link onReadyRefreshed} so consumers see a fresh AST. Wraps the
|
|
@@ -412,31 +514,21 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
412
514
|
protected async readyWithTimeout(state: DocumentState): Promise<void> {
|
|
413
515
|
const uri = this._sourceUri;
|
|
414
516
|
const stopwatch = this.sharedServices.Clock.stopwatch();
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
`Missed '${DocumentState[state]}' notification after ${elapsed}ms; document is already at state ` +
|
|
426
|
-
`'${DocumentState[doc.state]}'. Likely a Langium build-phase event race.`
|
|
427
|
-
);
|
|
428
|
-
resolve();
|
|
429
|
-
} else {
|
|
430
|
-
reject(this.buildReadyTimeoutError(state, elapsed));
|
|
431
|
-
}
|
|
432
|
-
}, this.readyTimeoutMs);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
try {
|
|
436
|
-
await Promise.race([this.sharedServices.model.ModelService.waitForDocumentState(uri, state), timeout]);
|
|
437
|
-
} finally {
|
|
438
|
-
timer?.dispose();
|
|
517
|
+
const reached = this.sharedServices.model.ModelService.waitForDocumentState(uri, state);
|
|
518
|
+
if ((await this.sharedServices.Clock.raceTimer(reached, this.readyTimeoutMs)) !== TIMED_OUT) {
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
const elapsed = Math.round(stopwatch.elapsedMs);
|
|
522
|
+
// Through the model service's canonicalizing gateway, so a symlinked
|
|
523
|
+
// `_sourceUri` is not reported as a hard timeout.
|
|
524
|
+
const doc = this.sharedServices.model.ModelService.getDocument(uri);
|
|
525
|
+
if (!doc || doc.state < state) {
|
|
526
|
+
throw this.buildReadyTimeoutError(state, elapsed);
|
|
439
527
|
}
|
|
528
|
+
this.logger.warn(
|
|
529
|
+
`Missed '${DocumentState[state]}' notification after ${elapsed}ms; document is already at state ` +
|
|
530
|
+
`'${DocumentState[doc.state]}'. Likely a Langium build-phase event race.`
|
|
531
|
+
);
|
|
440
532
|
}
|
|
441
533
|
|
|
442
534
|
/**
|
|
@@ -491,13 +583,22 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
491
583
|
* `JsonModelState` shape meet the framework state via structural
|
|
492
584
|
* intersection at the recording-command call site.
|
|
493
585
|
*
|
|
494
|
-
* `
|
|
495
|
-
*
|
|
496
|
-
*
|
|
497
|
-
*
|
|
498
|
-
*
|
|
499
|
-
*
|
|
500
|
-
*
|
|
586
|
+
* `basedOn` is the snapshot version taken at command start by
|
|
587
|
+
* `HydraniumGlspRecordingCommand.execute`. Adopters that write through the
|
|
588
|
+
* diagram's session forward it as the args' `basedOn` field; adopters whose
|
|
589
|
+
* write path doesn't go through the session ignore it.
|
|
590
|
+
* `'anything'` when the writer is not the recording command (an external
|
|
591
|
+
* storage refresh, an undo replaying a recorded patch), which is a write
|
|
592
|
+
* authored against no particular server version.
|
|
593
|
+
*
|
|
594
|
+
* **Optional, and every implementation must default it to {@link basedOn}
|
|
595
|
+
* rather than to `'anything'`.** Optional is forced: GLSP's `JsonModelState`
|
|
596
|
+
* declares a one-parameter `updateSourceModel` and calls it with one
|
|
597
|
+
* argument, so a second required parameter makes the state unassignable to
|
|
598
|
+
* the slot it has to fill. Defaulting to the state's own snapshot version is
|
|
599
|
+
* what keeps that from costing anything — an operation handler calling
|
|
600
|
+
* `updateSourceModel(model)` gets the gate, and skipping it has to be typed
|
|
601
|
+
* out as `'anything'`.
|
|
501
602
|
*/
|
|
502
|
-
abstract updateSourceModel(model: TSourceModel,
|
|
603
|
+
abstract updateSourceModel(model: TSourceModel, basedOn?: BasedOn): MaybePromise<void>;
|
|
503
604
|
}
|
|
@@ -11,7 +11,7 @@ import { type JsonModelState } from '@eclipse-glsp/server';
|
|
|
11
11
|
import { injectable } from 'inversify';
|
|
12
12
|
import { type AstNode } from '@hydranium/langium';
|
|
13
13
|
import { URI } from '@hydranium/langium';
|
|
14
|
-
import { isPromiseLike, type MaybePromise } from '@hydranium/protocol';
|
|
14
|
+
import { type BasedOn, isPromiseLike, type MaybePromise } from '@hydranium/protocol';
|
|
15
15
|
import { AbstractHydraniumGlspState } from './abstract-hydranium-glsp-state.js';
|
|
16
16
|
|
|
17
17
|
/** Whole-document-text source model: the serialised text of the source root. */
|
|
@@ -32,11 +32,11 @@ export interface FullTextSourceModel {
|
|
|
32
32
|
* `serializer.Serializer` reached via `sharedServices.ServiceRegistry`
|
|
33
33
|
* (so multi-grammar workspaces route to the right serializer). `MaybePromise`
|
|
34
34
|
* because serialization may be async; the sync fast path is preserved.
|
|
35
|
-
* - {@link updateSourceModel} pushes the text back through
|
|
36
|
-
* `
|
|
37
|
-
*
|
|
35
|
+
* - {@link updateSourceModel} pushes the text back through the diagram's
|
|
36
|
+
* session (`update` accepts a raw text payload) and captures the re-parsed
|
|
37
|
+
* root, gated on the caller's `basedOn`; it throws without a session.
|
|
38
38
|
*
|
|
39
|
-
* No `
|
|
39
|
+
* No `base` / conflict reconcile: a whole-document model has exactly one
|
|
40
40
|
* field, so every concurrent edit is a same-field collision and undo / redo
|
|
41
41
|
* degrade to drop-on-divergence. Adopters that need field-level undo use
|
|
42
42
|
* `ReconcilingTransferHydraniumGlspState` instead.
|
|
@@ -52,12 +52,8 @@ export class FullTextHydraniumGlspState<TRoot extends AstNode>
|
|
|
52
52
|
return isPromiseLike(text) ? text.then(value => ({ text: value })) : { text };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
async updateSourceModel(model: FullTextSourceModel): Promise<void> {
|
|
56
|
-
const document = await this.
|
|
57
|
-
uri: this._sourceUri,
|
|
58
|
-
model: model.text,
|
|
59
|
-
clientId: this.clientId
|
|
60
|
-
});
|
|
55
|
+
async updateSourceModel(model: FullTextSourceModel, basedOn: BasedOn = this.basedOn): Promise<void> {
|
|
56
|
+
const document = await this.requireModelSession().update({ uri: this._sourceUri, model: model.text, basedOn });
|
|
61
57
|
this.setSourceRoot(this._sourceUri, document.root as TRoot);
|
|
62
58
|
}
|
|
63
59
|
}
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
-
import { type ConflictResolver, type Logger, isConflictError } from '@hydranium/protocol';
|
|
10
|
+
import { type BasedOn, type ConflictResolver, type Logger, isConflictError } from '@hydranium/protocol';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* The I/O and policy a {@link reconcileSourceModelWrite} call needs. Deliberately
|
|
@@ -18,15 +18,16 @@ import { type ConflictResolver, type Logger, isConflictError } from '@hydranium/
|
|
|
18
18
|
*/
|
|
19
19
|
export interface SourceModelWriteHooks<TModel> {
|
|
20
20
|
/**
|
|
21
|
-
* Write `model` and
|
|
22
|
-
* on the first attempt (opting into the `ConflictError` gate) and
|
|
23
|
-
* on the merged / forced retries, where the
|
|
21
|
+
* Write `model` and record the resulting state. Called with the caller's own
|
|
22
|
+
* `basedOn` on the first attempt (opting into the `ConflictError` gate) and
|
|
23
|
+
* with `'anything'` on the merged / forced retries, where the reconcile has
|
|
24
|
+
* already decided to win and the point is to land the write.
|
|
24
25
|
*/
|
|
25
|
-
persist(model: TModel,
|
|
26
|
+
persist(model: TModel, basedOn: BasedOn): Promise<void>;
|
|
26
27
|
/** Current server-side projection, or `undefined` when unavailable. */
|
|
27
28
|
refetch(): Promise<TModel | undefined>;
|
|
28
|
-
/**
|
|
29
|
-
readonly
|
|
29
|
+
/** The last in-sync projection the user's intent is measured against. */
|
|
30
|
+
readonly base: TModel | undefined;
|
|
30
31
|
readonly conflictResolver: ConflictResolver;
|
|
31
32
|
readonly logger: Logger;
|
|
32
33
|
/** Invoked when the edit is dropped, so the caller can resync its own state. */
|
|
@@ -44,33 +45,35 @@ export interface SourceModelWriteHooks<TModel> {
|
|
|
44
45
|
*/
|
|
45
46
|
export async function reconcileSourceModelWrite<TModel extends object>(
|
|
46
47
|
model: TModel,
|
|
47
|
-
|
|
48
|
+
basedOn: BasedOn,
|
|
48
49
|
hooks: SourceModelWriteHooks<TModel>
|
|
49
50
|
): Promise<void> {
|
|
50
51
|
try {
|
|
51
|
-
await hooks.persist(model,
|
|
52
|
+
await hooks.persist(model, basedOn);
|
|
52
53
|
} catch (err) {
|
|
53
54
|
if (!isConflictError(err)) {
|
|
54
55
|
throw err;
|
|
55
56
|
}
|
|
56
|
-
const outcome = await hooks.conflictResolver.resolve(hooks.
|
|
57
|
+
const outcome = await hooks.conflictResolver.resolve(hooks.base ?? model, model, () => hooks.refetch());
|
|
57
58
|
switch (outcome.status) {
|
|
58
59
|
case 'merged':
|
|
59
|
-
await hooks.persist(outcome.merged);
|
|
60
|
+
await hooks.persist(outcome.merged, 'anything');
|
|
60
61
|
return;
|
|
61
62
|
case 'no-op':
|
|
62
|
-
hooks.logger.debug(`updateSourceModel no-op (v${err.
|
|
63
|
+
hooks.logger.debug(`updateSourceModel no-op (v${err.expectedVersion} → v${err.actualVersion}); already in sync`);
|
|
63
64
|
return;
|
|
64
65
|
case 'conflict':
|
|
65
66
|
hooks.logger.warn(
|
|
66
|
-
`updateSourceModel conflict (v${err.
|
|
67
|
+
`updateSourceModel conflict (v${err.expectedVersion} → v${err.actualVersion}); dropping the diagram edit — ` +
|
|
67
68
|
'a foreign writer changed the same field'
|
|
68
69
|
);
|
|
69
70
|
hooks.onConflictDropped();
|
|
70
71
|
return;
|
|
71
72
|
case 'unavailable':
|
|
72
|
-
hooks.logger.warn(
|
|
73
|
-
|
|
73
|
+
hooks.logger.warn(
|
|
74
|
+
`updateSourceModel refetch unavailable (v${err.expectedVersion} → v${err.actualVersion}); forcing without version`
|
|
75
|
+
);
|
|
76
|
+
await hooks.persist(model, 'anything');
|
|
74
77
|
return;
|
|
75
78
|
}
|
|
76
79
|
}
|