@hydranium/glsp-server 1.0.0-next.123 → 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 +8 -4
- 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/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 +6 -3
- package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -1
- package/lib/launcher/hydranium-glsp-server.js +12 -3
- package/lib/launcher/hydranium-glsp-server.js.map +1 -1
- 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 +1 -1
- package/lib/node/start-glsp-server.js.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -1
- package/lib/state/abstract-hydranium-glsp-state.js +12 -22
- package/lib/state/abstract-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 +25 -4
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -1
- package/lib/storage/hydranium-glsp-storage.js +57 -12
- package/lib/storage/hydranium-glsp-storage.js.map +1 -1
- package/lib/storage/index.d.ts +1 -0
- package/lib/storage/index.d.ts.map +1 -1
- package/lib/storage/index.js +1 -0
- package/lib/storage/index.js.map +1 -1
- package/package.json +7 -7
- package/src/browser/start-glsp-server-in-worker.ts +41 -44
- package/src/command/hydranium-glsp-command-stack.ts +36 -0
- 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 +12 -3
- package/src/node/start-glsp-server.ts +1 -2
- package/src/state/abstract-hydranium-glsp-state.ts +16 -26
- package/src/storage/hydranium-glsp-request-save-model-action-handler.ts +40 -0
- package/src/storage/hydranium-glsp-storage.ts +61 -15
- package/src/storage/index.ts +1 -0
|
@@ -14,43 +14,38 @@ import {
|
|
|
14
14
|
WorkerServerLauncher,
|
|
15
15
|
createAppModule
|
|
16
16
|
} from '@eclipse-glsp/server/browser.js';
|
|
17
|
-
import { Container, type ContainerModule } from 'inversify';
|
|
18
|
-
import type
|
|
17
|
+
import { Container, injectable, type ContainerModule } from 'inversify';
|
|
18
|
+
import { createMessagePortTransport, type Logger, type TransferredMessagePort } from '@hydranium/protocol';
|
|
19
|
+
import { createMessageConnection, type MessageConnection } from 'vscode-jsonrpc/browser';
|
|
19
20
|
import type { IntegratedServer } from '@hydranium/core';
|
|
20
21
|
import { createGlspFrameworkOverrides } from '../launcher/glsp-framework-overrides.js';
|
|
21
22
|
import { createGlspServerOverrides } from '../launcher/glsp-server-overrides.js';
|
|
22
23
|
|
|
24
|
+
// Re-exported so the type of this head's `context` still resolves from here.
|
|
25
|
+
export type { TransferredMessagePort } from '@hydranium/protocol';
|
|
26
|
+
|
|
23
27
|
/**
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* **The member set is chosen to admit a `MessagePort` and REJECT a `Worker` or
|
|
32
|
-
* the worker global**, both of which the launcher's own reader would otherwise
|
|
33
|
-
* accept. Only a port needs starting, so `start` is what tells the three apart,
|
|
34
|
-
* and naming it here turns "never bind a head to the global" from a rule in a
|
|
35
|
-
* document into a compile error at the call site.
|
|
28
|
+
* The worker launcher every {@link startGlspServerInWorker} head runs on: GLSP's
|
|
29
|
+
* own, with its connection built over `createMessagePortTransport` rather than
|
|
30
|
+
* over `BrowserMessageReader`/`BrowserMessageWriter`, which never report a
|
|
31
|
+
* close. So the client disposing its connection closes this one, and upstream
|
|
32
|
+
* disposes the server instance, and with it every client session, as it does
|
|
33
|
+
* when a socket closes.
|
|
36
34
|
*
|
|
37
|
-
*
|
|
38
|
-
* neither `MessagePort` nor `Worker` as a type, so nothing in this package can
|
|
39
|
-
* demonstrate the rejection; a host compiling its worker against `lib.webworker`
|
|
40
|
-
* (or a page against `lib.dom`) is where the names resolve and the guard bites.
|
|
41
|
-
* Measured there: passing the worker global fails with `Property 'start' is
|
|
42
|
-
* missing in type 'DedicatedWorkerGlobalScope'`.
|
|
35
|
+
* Requires `context`: unlike upstream it never falls back to the worker global.
|
|
43
36
|
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
*
|
|
47
|
-
*
|
|
48
|
-
*
|
|
37
|
+
* **The install needs a single `vscode-jsonrpc`.** The connection comes from
|
|
38
|
+
* this package's copy, and GLSP's request and notification types from the copy
|
|
39
|
+
* `@eclipse-glsp/protocol` resolves. With two copies, a typed notification or
|
|
40
|
+
* request GLSP sends throws `Unknown parameter structure auto`, because the
|
|
41
|
+
* connection compares the type's parameter-structure marker by identity.
|
|
49
42
|
*/
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
43
|
+
@injectable()
|
|
44
|
+
export class HydraniumGlspWorkerServerLauncher extends WorkerServerLauncher {
|
|
45
|
+
protected override createConnection(options: WorkerLaunchOptions): MessageConnection {
|
|
46
|
+
const transport = createMessagePortTransport(options.context as unknown as TransferredMessagePort);
|
|
47
|
+
return createMessageConnection(transport.reader, transport.writer);
|
|
48
|
+
}
|
|
54
49
|
}
|
|
55
50
|
|
|
56
51
|
/**
|
|
@@ -66,14 +61,15 @@ export interface BrowserGlspServerOptions {
|
|
|
66
61
|
/**
|
|
67
62
|
* The port the host transferred into the worker for this head.
|
|
68
63
|
*
|
|
69
|
-
* **Required
|
|
70
|
-
*
|
|
71
|
-
*
|
|
72
|
-
*
|
|
73
|
-
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
*
|
|
64
|
+
* **Required: the worker global is no substitute.** A head on the global
|
|
65
|
+
* receives every other head's traffic — a reader filters nothing, so this is
|
|
66
|
+
* not a race under load but every message delivered to the wrong reader — and
|
|
67
|
+
* GLSP's launcher posts its startup string through the global `postMessage`
|
|
68
|
+
* regardless of the connection it was given, which no JSON-RPC reader parses.
|
|
69
|
+
* Unlike upstream's, this head's launcher never falls back to the global.
|
|
70
|
+
*
|
|
71
|
+
* The page's end must connect through `createMessagePortTransport` as well:
|
|
72
|
+
* the head reads its close signal from the port, and posts one on it.
|
|
77
73
|
*/
|
|
78
74
|
readonly context: TransferredMessagePort;
|
|
79
75
|
/**
|
|
@@ -126,8 +122,9 @@ export interface BrowserGlspServerOptions {
|
|
|
126
122
|
* below. There is no transport handshake to wait on: the port arrived live,
|
|
127
123
|
* and the launcher's own readiness signal is a string it posts on the worker
|
|
128
124
|
* global, which nothing here reads.
|
|
129
|
-
* - `stopped` resolves when the connection closes,
|
|
130
|
-
* error.
|
|
125
|
+
* - `stopped` resolves when the connection closes, which the client disposing
|
|
126
|
+
* its connection causes, and rejects on a connection error. Nothing reports a
|
|
127
|
+
* page that dies, because the port cannot.
|
|
131
128
|
*/
|
|
132
129
|
export function startGlspServerInWorker(options: BrowserGlspServerOptions): IntegratedServer {
|
|
133
130
|
const lifecycle = options.logger;
|
|
@@ -145,7 +142,7 @@ export function startGlspServerInWorker(options: BrowserGlspServerOptions): Inte
|
|
|
145
142
|
const appContainer = new Container();
|
|
146
143
|
appContainer.load(glspAppModule, createGlspFrameworkOverrides(options.createLogger), ...(options.appModules ?? []));
|
|
147
144
|
|
|
148
|
-
const launcher = appContainer.resolve<WorkerServerLauncher>(
|
|
145
|
+
const launcher = appContainer.resolve<WorkerServerLauncher>(HydraniumGlspWorkerServerLauncher);
|
|
149
146
|
// Additional module rather than an app-container binding, as on Node: the
|
|
150
147
|
// launcher loads these into the per-connection SERVER container, the only
|
|
151
148
|
// tier where the adopter's `ServerModule` binding of `GLSPServer` is
|
|
@@ -153,10 +150,10 @@ export function startGlspServerInWorker(options: BrowserGlspServerOptions): Inte
|
|
|
153
150
|
launcher.configure(options.serverModule, createGlspServerOverrides());
|
|
154
151
|
|
|
155
152
|
try {
|
|
156
|
-
// Upstream declares `WorkerLaunchOptions.context` as `Worker`,
|
|
157
|
-
//
|
|
158
|
-
//
|
|
159
|
-
//
|
|
153
|
+
// Upstream declares `WorkerLaunchOptions.context` as `Worker`, and
|
|
154
|
+
// `HydraniumGlspWorkerServerLauncher.createConnection` reads it as a
|
|
155
|
+
// port — so the value that is CORRECT here is the one the declaration
|
|
156
|
+
// names as wrong.
|
|
160
157
|
//
|
|
161
158
|
// **The cast is currently redundant, and is kept deliberately.** `Worker`
|
|
162
159
|
// does not resolve under this package's `lib`, so `skipLibCheck` leaves
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { DefaultCommandStack, ModelState } from '@eclipse-glsp/server';
|
|
11
|
+
import { type ServerSharedServices } from '@hydranium/core';
|
|
12
|
+
import { inject, injectable } from 'inversify';
|
|
13
|
+
import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* GLSP's command stack with the diagram's dirty state read from the text store:
|
|
17
|
+
* dirty while any document the diagram's client session has open differs from
|
|
18
|
+
* its file, as the store last knew the file. Undo and redo are GLSP's.
|
|
19
|
+
*
|
|
20
|
+
* GLSP's own answer counts the diagram's commands since its last save, so it
|
|
21
|
+
* misses every change the diagram did not make: another client's unsaved edit
|
|
22
|
+
* leaves it clean, and a save it did not make of a document other than the
|
|
23
|
+
* diagram's own leaves it dirty. Every `SetDirtyStateAction` GLSP sends reads
|
|
24
|
+
* this answer.
|
|
25
|
+
*/
|
|
26
|
+
@injectable()
|
|
27
|
+
export class HydraniumGlspCommandStack extends DefaultCommandStack {
|
|
28
|
+
@inject(ModelState) protected readonly modelState!: ModelState;
|
|
29
|
+
@inject(HydraniumTypes.SharedCoreServices) protected readonly sharedServices!: ServerSharedServices;
|
|
30
|
+
|
|
31
|
+
override get isDirty(): boolean {
|
|
32
|
+
const textDocuments = this.sharedServices.workspace.TextDocuments;
|
|
33
|
+
const clientId = this.modelState.clientId;
|
|
34
|
+
return textDocuments.openDocuments().some(open => open.clients.includes(clientId) && textDocuments.isDirty(open.uri));
|
|
35
|
+
}
|
|
36
|
+
}
|
package/src/command/index.ts
CHANGED
|
@@ -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
|
/**
|
|
@@ -64,9 +65,12 @@ export class HydraniumGlspServer extends DefaultGLSPServer {
|
|
|
64
65
|
/**
|
|
65
66
|
* Lifted verbatim from `@eclipse-glsp/server` 2.7.0's
|
|
66
67
|
* {@link DefaultGLSPServer.handleClientRequest}, with the detail computation
|
|
67
|
-
* delegated to {@link requestFailureDetail}
|
|
68
|
-
*
|
|
69
|
-
*
|
|
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.
|
|
70
74
|
*
|
|
71
75
|
* **This body does not track upstream.** A release that changes request
|
|
72
76
|
* handling would silently not apply, on the path every request takes. There
|
|
@@ -97,6 +101,11 @@ export class HydraniumGlspServer extends DefaultGLSPServer {
|
|
|
97
101
|
detail
|
|
98
102
|
});
|
|
99
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
|
+
}
|
|
100
109
|
} catch (sendError: unknown) {
|
|
101
110
|
this.logger.error(`Failed to send rejection for request '${action.requestId}':`, sendError);
|
|
102
111
|
}
|
|
@@ -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';
|
|
@@ -24,11 +24,11 @@ import {
|
|
|
24
24
|
type BasedOn,
|
|
25
25
|
type SnapshotVersion,
|
|
26
26
|
type ConflictResolver,
|
|
27
|
-
type Disposable,
|
|
28
27
|
type Logger,
|
|
29
28
|
type Tracer,
|
|
30
29
|
asSnapshotVersion,
|
|
31
|
-
NO_MATCHING_VERSION
|
|
30
|
+
NO_MATCHING_VERSION,
|
|
31
|
+
TIMED_OUT
|
|
32
32
|
} from '@hydranium/protocol';
|
|
33
33
|
import { type HydraniumGlspIndex } from './hydranium-glsp-index.js';
|
|
34
34
|
import { HydraniumTypes } from './hydranium-shared-core-services.js';
|
|
@@ -514,31 +514,21 @@ export abstract class AbstractHydraniumGlspState<TRoot extends AstNode, TSourceM
|
|
|
514
514
|
protected async readyWithTimeout(state: DocumentState): Promise<void> {
|
|
515
515
|
const uri = this._sourceUri;
|
|
516
516
|
const stopwatch = this.sharedServices.Clock.stopwatch();
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
`Missed '${DocumentState[state]}' notification after ${elapsed}ms; document is already at state ` +
|
|
528
|
-
`'${DocumentState[doc.state]}'. Likely a Langium build-phase event race.`
|
|
529
|
-
);
|
|
530
|
-
resolve();
|
|
531
|
-
} else {
|
|
532
|
-
reject(this.buildReadyTimeoutError(state, elapsed));
|
|
533
|
-
}
|
|
534
|
-
}, this.readyTimeoutMs);
|
|
535
|
-
});
|
|
536
|
-
|
|
537
|
-
try {
|
|
538
|
-
await Promise.race([this.sharedServices.model.ModelService.waitForDocumentState(uri, state), timeout]);
|
|
539
|
-
} finally {
|
|
540
|
-
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);
|
|
541
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
|
+
);
|
|
542
532
|
}
|
|
543
533
|
|
|
544
534
|
/**
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
import { type Action, SaveModelAction, SaveModelActionHandler } from '@eclipse-glsp/server';
|
|
11
|
+
import { ModelSavedAction, RequestSaveModelAction } from '@hydranium/protocol';
|
|
12
|
+
import { injectable } from 'inversify';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Answers a {@link RequestSaveModelAction}: saves exactly as GLSP's
|
|
16
|
+
* {@link SaveModelActionHandler} saves a `SaveModelAction` carrying the same
|
|
17
|
+
* `fileUri`, dirty-state change included, then replies with a
|
|
18
|
+
* {@link ModelSavedAction}.
|
|
19
|
+
*
|
|
20
|
+
* A failed save throws, and the server turns the throw into a `RejectAction`
|
|
21
|
+
* for the request. The save handler's own `SaveModelAction` stays registered,
|
|
22
|
+
* so a client that sends it is unaffected.
|
|
23
|
+
*
|
|
24
|
+
* `AbstractHydraniumGlspDiagramModule` registers it. A diagram module that does
|
|
25
|
+
* not extend that base adds it in `configureActionHandlers`. Without it no
|
|
26
|
+
* handler is advertised for the request, and the Theia client saves as GLSP's
|
|
27
|
+
* saveable does: a save nothing answers, so a failed one surfaces only as a
|
|
28
|
+
* timeout, and a window can close over a save still under way.
|
|
29
|
+
*/
|
|
30
|
+
@injectable()
|
|
31
|
+
export class HydraniumGlspRequestSaveModelActionHandler extends SaveModelActionHandler {
|
|
32
|
+
override actionKinds = [RequestSaveModelAction.KIND];
|
|
33
|
+
|
|
34
|
+
// Typed `Action`, the one parameter type both this kind and the base's accept.
|
|
35
|
+
override async execute(action: Action): Promise<Action[]> {
|
|
36
|
+
const fileUri = 'fileUri' in action && typeof action.fileUri === 'string' ? action.fileUri : undefined;
|
|
37
|
+
const actions = await super.execute(SaveModelAction.create({ fileUri }));
|
|
38
|
+
return [...actions, ModelSavedAction.create()];
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
type RequestModelAction,
|
|
28
28
|
SOURCE_URI_ARG,
|
|
29
29
|
type SaveModelAction,
|
|
30
|
+
SetDirtyStateAction,
|
|
30
31
|
SetEditModeAction,
|
|
31
32
|
type SourceModelStorage,
|
|
32
33
|
TEMPORARY_CLIENT_ID
|
|
@@ -47,6 +48,7 @@ import {
|
|
|
47
48
|
type AstDocumentSavedEvent,
|
|
48
49
|
type AstDocumentUpdatedEvent,
|
|
49
50
|
type ClientSession as ModelClientSession,
|
|
51
|
+
type DocumentDirtyChangedEvent,
|
|
50
52
|
type ServerSharedServices
|
|
51
53
|
} from '@hydranium/core';
|
|
52
54
|
import { DiagnosticSeverity } from 'vscode-languageserver-types';
|
|
@@ -262,6 +264,8 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
262
264
|
|
|
263
265
|
/** Set by {@link dispose}; a disposed storage registers no session again. */
|
|
264
266
|
protected disposed = false;
|
|
267
|
+
/** Saves of the diagram's own that GLSP's save handler awaits; see {@link handleDirtyChanged}. */
|
|
268
|
+
protected ownSavesPending = 0;
|
|
265
269
|
|
|
266
270
|
/**
|
|
267
271
|
* Whether an external resubmit has ever been dispatched. Gates the
|
|
@@ -329,10 +333,7 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
329
333
|
*/
|
|
330
334
|
protected requireModelSession(): ModelClientSession<AstNode> {
|
|
331
335
|
if (this.disposed) {
|
|
332
|
-
throw new SessionClosedError(
|
|
333
|
-
this.state.clientId,
|
|
334
|
-
`The diagram of ${this.state.clientId} has closed; its client session has ended`
|
|
335
|
-
);
|
|
336
|
+
throw new SessionClosedError(this.state.clientId, 'The diagram has closed; its client session has ended.');
|
|
336
337
|
}
|
|
337
338
|
if (!this.state.modelSession) {
|
|
338
339
|
this.state.modelSession = this.registerModelSession();
|
|
@@ -427,9 +428,12 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
427
428
|
// React to external rebuilds: settle-gated capture + debounced/deduped resubmit.
|
|
428
429
|
this.toDispose.push(modelService.onModelUpdated(rootUri, event => this.handleModelUpdated(rootUri, event)));
|
|
429
430
|
|
|
430
|
-
//
|
|
431
|
+
// GLSP's own command stack counts commands; tell it of another client's save.
|
|
431
432
|
this.toDispose.push(modelService.onModelSaved(rootUri, event => this.handleModelSaved(event)));
|
|
432
433
|
|
|
434
|
+
// Tell the client of each dirty flip, one the diagram did not cause included.
|
|
435
|
+
this.toDispose.push(this.sharedServices.workspace.TextDocuments.onDidChangeDirty(event => this.handleDirtyChanged(event)));
|
|
436
|
+
|
|
433
437
|
// Capture the initial settled root.
|
|
434
438
|
const document = await modelService.settled(rootUri);
|
|
435
439
|
await this.captureSettledRoot(rootUri, document);
|
|
@@ -650,9 +654,10 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
650
654
|
}
|
|
651
655
|
|
|
652
656
|
/**
|
|
653
|
-
*
|
|
654
|
-
* mark
|
|
655
|
-
*
|
|
657
|
+
* For a diagram module on GLSP's own command stack, which counts commands:
|
|
658
|
+
* mark it clean when another client persists the document.
|
|
659
|
+
* `HydraniumGlspCommandStack` reads the text store instead, and this does
|
|
660
|
+
* not move it.
|
|
656
661
|
*/
|
|
657
662
|
protected handleModelSaved(event: AstDocumentSavedEvent<AstNode>): void {
|
|
658
663
|
if (this.state.clientId !== event.sourceClientId) {
|
|
@@ -660,6 +665,35 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
660
665
|
}
|
|
661
666
|
}
|
|
662
667
|
|
|
668
|
+
/**
|
|
669
|
+
* Send the diagram's dirty state when a document it has open turns dirty or
|
|
670
|
+
* clean, the answer being the command stack's. GLSP sends it only with a
|
|
671
|
+
* model submission or a save of the diagram's own, so a save by another
|
|
672
|
+
* client, an editor included, or an edit that changes nothing the diagram
|
|
673
|
+
* shows would otherwise leave the client's marker wrong until the next
|
|
674
|
+
* gesture. A flip the diagram's own operation causes is sent twice, once
|
|
675
|
+
* here and once with the submission, and the client keeps the one state.
|
|
676
|
+
*
|
|
677
|
+
* Nothing is sent while a save of the diagram's own is awaited: GLSP's save
|
|
678
|
+
* handler sends the state once the save settles, reason `save`, and GLSP's
|
|
679
|
+
* client keeps a state only when it changes, so the save's own flip sent
|
|
680
|
+
* here first would leave that answer changing nothing. GLSP's saveable
|
|
681
|
+
* waits for exactly that answer, and times out without it.
|
|
682
|
+
*/
|
|
683
|
+
protected handleDirtyChanged(event: DocumentDirtyChangedEvent): void {
|
|
684
|
+
if (this.ownSavesPending > 0 || !this.sharedServices.workspace.TextDocuments.isOpenInClient(event.uri, this.state.clientId)) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
this.sendDirtyState();
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
/** Send the command stack's dirty state, reason `external`. */
|
|
691
|
+
protected sendDirtyState(): void {
|
|
692
|
+
this.actionDispatcher
|
|
693
|
+
.dispatch(SetDirtyStateAction.create(this.commandStack.isDirty, { reason: 'external' }))
|
|
694
|
+
.catch((error: unknown) => this.logger.warn(`Could not send the dirty state of ${this.state.sourceUri}: ${String(error)}`));
|
|
695
|
+
}
|
|
696
|
+
|
|
663
697
|
/**
|
|
664
698
|
* Re-settle to a guaranteed fully-linked + reprojected root, capture it, and
|
|
665
699
|
* (unless suppressed) resubmit a deduped external GModel. Diagnostics for the
|
|
@@ -831,17 +865,29 @@ export class HydraniumGlspStorage<TRoot extends AstNode, TSourceModel = string>
|
|
|
831
865
|
// the call site rather than inside `getFileUri` so that method's contract —
|
|
832
866
|
// return what the action said — is unchanged for adopters overriding it.
|
|
833
867
|
const uri = this.toSourceModelUri(this.getFileUri(action));
|
|
834
|
-
|
|
835
|
-
const persisted = this.flushWriteSet(uri);
|
|
836
|
-
|
|
837
|
-
if (policy.kind === 'fire-and-forget') {
|
|
868
|
+
if (this.saveDeliveryPolicy.kind === 'fire-and-forget') {
|
|
838
869
|
// Log rather than leave an unhandled rejection: the promise is not
|
|
839
870
|
// returned, so nothing else will observe a failure.
|
|
840
|
-
|
|
871
|
+
this.flushWriteSet(uri).catch(error =>
|
|
872
|
+
this.logger.error(`Save failed for ${uri}: ${error instanceof Error ? error.message : String(error)}`)
|
|
873
|
+
);
|
|
841
874
|
return undefined;
|
|
842
875
|
}
|
|
843
|
-
// Awaited: any failure propagates to GLSP's save-action handler
|
|
844
|
-
|
|
876
|
+
// Awaited: any failure propagates to GLSP's save-action handler, which
|
|
877
|
+
// then sends no dirty state, so the one the save held back goes here.
|
|
878
|
+
// An override that throws before it returns a promise throws inside the
|
|
879
|
+
// async function too, so the hold always ends.
|
|
880
|
+
this.ownSavesPending++;
|
|
881
|
+
return (async () => {
|
|
882
|
+
try {
|
|
883
|
+
await this.flushWriteSet(uri);
|
|
884
|
+
} catch (error: unknown) {
|
|
885
|
+
this.sendDirtyState();
|
|
886
|
+
throw error;
|
|
887
|
+
} finally {
|
|
888
|
+
this.ownSavesPending--;
|
|
889
|
+
}
|
|
890
|
+
})();
|
|
845
891
|
}
|
|
846
892
|
|
|
847
893
|
/**
|
package/src/storage/index.ts
CHANGED
|
@@ -7,5 +7,6 @@
|
|
|
7
7
|
* SPDX-License-Identifier: MIT
|
|
8
8
|
********************************************************************************/
|
|
9
9
|
|
|
10
|
+
export * from './hydranium-glsp-request-save-model-action-handler.js';
|
|
10
11
|
export * from './hydranium-glsp-storage.js';
|
|
11
12
|
export * from './save-delivery-policy.js';
|