@hydranium/glsp-server 1.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +97 -0
- package/lib/browser/index.d.ts +10 -0
- package/lib/browser/index.d.ts.map +1 -0
- package/lib/browser/index.js +15 -0
- package/lib/browser/index.js.map +1 -0
- package/lib/browser/start-glsp-server-in-worker.d.ts +121 -0
- package/lib/browser/start-glsp-server-in-worker.d.ts.map +1 -0
- package/lib/browser/start-glsp-server-in-worker.js +102 -0
- package/lib/browser/start-glsp-server-in-worker.js.map +1 -0
- package/lib/command/hydranium-glsp-recording-command.d.ts +132 -0
- package/lib/command/hydranium-glsp-recording-command.d.ts.map +1 -0
- package/lib/command/hydranium-glsp-recording-command.js +159 -0
- package/lib/command/hydranium-glsp-recording-command.js.map +1 -0
- package/lib/command/index.d.ts +10 -0
- package/lib/command/index.d.ts.map +1 -0
- package/lib/command/index.js +10 -0
- package/lib/command/index.js.map +1 -0
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts +61 -0
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.d.ts.map +1 -0
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js +93 -0
- package/lib/computed-bounds/hydranium-glsp-computed-bounds-action-handler.js.map +1 -0
- package/lib/computed-bounds/index.d.ts +10 -0
- package/lib/computed-bounds/index.d.ts.map +1 -0
- package/lib/computed-bounds/index.js +10 -0
- package/lib/computed-bounds/index.js.map +1 -0
- package/lib/dispatcher/index.d.ts +10 -0
- package/lib/dispatcher/index.d.ts.map +1 -0
- package/lib/dispatcher/index.js +10 -0
- package/lib/dispatcher/index.js.map +1 -0
- package/lib/dispatcher/server-action-dispatcher.d.ts +52 -0
- package/lib/dispatcher/server-action-dispatcher.d.ts.map +1 -0
- package/lib/dispatcher/server-action-dispatcher.js +79 -0
- package/lib/dispatcher/server-action-dispatcher.js.map +1 -0
- package/lib/index.d.ts +19 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +20 -0
- package/lib/index.js.map +1 -0
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts +75 -0
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.d.ts.map +1 -0
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js +100 -0
- package/lib/launcher/abstract-hydranium-glsp-diagram-module.js.map +1 -0
- package/lib/launcher/glsp-app-module.d.ts +87 -0
- package/lib/launcher/glsp-app-module.d.ts.map +1 -0
- package/lib/launcher/glsp-app-module.js +71 -0
- package/lib/launcher/glsp-app-module.js.map +1 -0
- package/lib/launcher/glsp-framework-overrides.d.ts +32 -0
- package/lib/launcher/glsp-framework-overrides.d.ts.map +1 -0
- package/lib/launcher/glsp-framework-overrides.js +49 -0
- package/lib/launcher/glsp-framework-overrides.js.map +1 -0
- package/lib/launcher/glsp-server-overrides.d.ts +39 -0
- package/lib/launcher/glsp-server-overrides.d.ts.map +1 -0
- package/lib/launcher/glsp-server-overrides.js +45 -0
- package/lib/launcher/glsp-server-overrides.js.map +1 -0
- package/lib/launcher/hydranium-glsp-server.d.ts +62 -0
- package/lib/launcher/hydranium-glsp-server.d.ts.map +1 -0
- package/lib/launcher/hydranium-glsp-server.js +101 -0
- package/lib/launcher/hydranium-glsp-server.js.map +1 -0
- package/lib/launcher/index.d.ts +12 -0
- package/lib/launcher/index.d.ts.map +1 -0
- package/lib/launcher/index.js +22 -0
- package/lib/launcher/index.js.map +1 -0
- package/lib/logging/glsp-client-logger.d.ts +110 -0
- package/lib/logging/glsp-client-logger.d.ts.map +1 -0
- package/lib/logging/glsp-client-logger.js +151 -0
- package/lib/logging/glsp-client-logger.js.map +1 -0
- package/lib/logging/index.d.ts +10 -0
- package/lib/logging/index.d.ts.map +1 -0
- package/lib/logging/index.js +10 -0
- package/lib/logging/index.js.map +1 -0
- package/lib/node/index.d.ts +10 -0
- package/lib/node/index.d.ts.map +1 -0
- package/lib/node/index.js +13 -0
- package/lib/node/index.js.map +1 -0
- package/lib/node/start-glsp-server.d.ts +122 -0
- package/lib/node/start-glsp-server.d.ts.map +1 -0
- package/lib/node/start-glsp-server.js +103 -0
- package/lib/node/start-glsp-server.js.map +1 -0
- package/lib/state/abstract-hydranium-glsp-state.d.ts +333 -0
- package/lib/state/abstract-hydranium-glsp-state.d.ts.map +1 -0
- package/lib/state/abstract-hydranium-glsp-state.js +451 -0
- package/lib/state/abstract-hydranium-glsp-state.js.map +1 -0
- package/lib/state/full-text-hydranium-glsp-state.d.ts +43 -0
- package/lib/state/full-text-hydranium-glsp-state.d.ts.map +1 -0
- package/lib/state/full-text-hydranium-glsp-state.js +60 -0
- package/lib/state/full-text-hydranium-glsp-state.js.map +1 -0
- package/lib/state/hydranium-glsp-index.d.ts +220 -0
- package/lib/state/hydranium-glsp-index.d.ts.map +1 -0
- package/lib/state/hydranium-glsp-index.js +320 -0
- package/lib/state/hydranium-glsp-index.js.map +1 -0
- package/lib/state/hydranium-shared-core-services.d.ts +90 -0
- package/lib/state/hydranium-shared-core-services.d.ts.map +1 -0
- package/lib/state/hydranium-shared-core-services.js +89 -0
- package/lib/state/hydranium-shared-core-services.js.map +1 -0
- package/lib/state/index.d.ts +17 -0
- package/lib/state/index.d.ts.map +1 -0
- package/lib/state/index.js +17 -0
- package/lib/state/index.js.map +1 -0
- package/lib/state/model-ready-timeout-error.d.ts +28 -0
- package/lib/state/model-ready-timeout-error.d.ts.map +1 -0
- package/lib/state/model-ready-timeout-error.js +36 -0
- package/lib/state/model-ready-timeout-error.js.map +1 -0
- package/lib/state/reconcile-source-model-write.d.ts +43 -0
- package/lib/state/reconcile-source-model-write.d.ts.map +1 -0
- package/lib/state/reconcile-source-model-write.js +47 -0
- package/lib/state/reconcile-source-model-write.js.map +1 -0
- package/lib/state/reconciling-multi-document-glsp-state.d.ts +164 -0
- package/lib/state/reconciling-multi-document-glsp-state.d.ts.map +1 -0
- package/lib/state/reconciling-multi-document-glsp-state.js +222 -0
- package/lib/state/reconciling-multi-document-glsp-state.js.map +1 -0
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts +91 -0
- package/lib/state/reconciling-transfer-hydranium-glsp-state.d.ts.map +1 -0
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js +129 -0
- package/lib/state/reconciling-transfer-hydranium-glsp-state.js.map +1 -0
- package/lib/storage/hydranium-glsp-storage.d.ts +348 -0
- package/lib/storage/hydranium-glsp-storage.d.ts.map +1 -0
- package/lib/storage/hydranium-glsp-storage.js +634 -0
- package/lib/storage/hydranium-glsp-storage.js.map +1 -0
- package/lib/storage/index.d.ts +11 -0
- package/lib/storage/index.d.ts.map +1 -0
- package/lib/storage/index.js +11 -0
- package/lib/storage/index.js.map +1 -0
- package/lib/storage/save-conflict-policy.d.ts +56 -0
- package/lib/storage/save-conflict-policy.d.ts.map +1 -0
- package/lib/storage/save-conflict-policy.js +19 -0
- package/lib/storage/save-conflict-policy.js.map +1 -0
- package/lib/submission/hydranium-glsp-submission-handler.d.ts +109 -0
- package/lib/submission/hydranium-glsp-submission-handler.d.ts.map +1 -0
- package/lib/submission/hydranium-glsp-submission-handler.js +157 -0
- package/lib/submission/hydranium-glsp-submission-handler.js.map +1 -0
- package/lib/submission/index.d.ts +10 -0
- package/lib/submission/index.d.ts.map +1 -0
- package/lib/submission/index.js +10 -0
- package/lib/submission/index.js.map +1 -0
- package/lib/testing/glsp-harness.d.ts +170 -0
- package/lib/testing/glsp-harness.d.ts.map +1 -0
- package/lib/testing/glsp-harness.js +231 -0
- package/lib/testing/glsp-harness.js.map +1 -0
- package/lib/testing/index.d.ts +11 -0
- package/lib/testing/index.d.ts.map +1 -0
- package/lib/testing/index.js +13 -0
- package/lib/testing/index.js.map +1 -0
- package/lib/testing/make-noop-glsp-logger.d.ts +42 -0
- package/lib/testing/make-noop-glsp-logger.d.ts.map +1 -0
- package/lib/testing/make-noop-glsp-logger.js +47 -0
- package/lib/testing/make-noop-glsp-logger.js.map +1 -0
- package/lib/util/index.d.ts +10 -0
- package/lib/util/index.d.ts.map +1 -0
- package/lib/util/index.js +10 -0
- package/lib/util/index.js.map +1 -0
- package/lib/util/service-identifier.d.ts +46 -0
- package/lib/util/service-identifier.d.ts.map +1 -0
- package/lib/util/service-identifier.js +47 -0
- package/lib/util/service-identifier.js.map +1 -0
- package/lib/validation/diagnostic-markers.d.ts +48 -0
- package/lib/validation/diagnostic-markers.d.ts.map +1 -0
- package/lib/validation/diagnostic-markers.js +68 -0
- package/lib/validation/diagnostic-markers.js.map +1 -0
- package/lib/validation/hydranium-glsp-model-validator.d.ts +51 -0
- package/lib/validation/hydranium-glsp-model-validator.d.ts.map +1 -0
- package/lib/validation/hydranium-glsp-model-validator.js +118 -0
- package/lib/validation/hydranium-glsp-model-validator.js.map +1 -0
- package/lib/validation/index.d.ts +11 -0
- package/lib/validation/index.d.ts.map +1 -0
- package/lib/validation/index.js +11 -0
- package/lib/validation/index.js.map +1 -0
- package/package.json +111 -0
- package/src/browser/index.ts +15 -0
- package/src/browser/start-glsp-server-in-worker.ts +202 -0
- package/src/command/hydranium-glsp-recording-command.ts +183 -0
- package/src/command/index.ts +10 -0
- package/src/computed-bounds/hydranium-glsp-computed-bounds-action-handler.ts +103 -0
- package/src/computed-bounds/index.ts +10 -0
- package/src/dispatcher/index.ts +10 -0
- package/src/dispatcher/server-action-dispatcher.ts +72 -0
- package/src/index.ts +20 -0
- package/src/launcher/abstract-hydranium-glsp-diagram-module.ts +117 -0
- package/src/launcher/glsp-app-module.ts +106 -0
- package/src/launcher/glsp-framework-overrides.ts +51 -0
- package/src/launcher/glsp-server-overrides.ts +46 -0
- package/src/launcher/hydranium-glsp-server.ts +104 -0
- package/src/launcher/index.ts +22 -0
- package/src/logging/glsp-client-logger.ts +184 -0
- package/src/logging/index.ts +10 -0
- package/src/node/index.ts +13 -0
- package/src/node/start-glsp-server.ts +214 -0
- package/src/state/abstract-hydranium-glsp-state.ts +503 -0
- package/src/state/full-text-hydranium-glsp-state.ts +63 -0
- package/src/state/hydranium-glsp-index.ts +337 -0
- package/src/state/hydranium-shared-core-services.ts +92 -0
- package/src/state/index.ts +17 -0
- package/src/state/model-ready-timeout-error.ts +33 -0
- package/src/state/reconcile-source-model-write.ts +77 -0
- package/src/state/reconciling-multi-document-glsp-state.ts +248 -0
- package/src/state/reconciling-transfer-hydranium-glsp-state.ts +132 -0
- package/src/storage/hydranium-glsp-storage.ts +660 -0
- package/src/storage/index.ts +11 -0
- package/src/storage/save-conflict-policy.ts +52 -0
- package/src/submission/hydranium-glsp-submission-handler.ts +170 -0
- package/src/submission/index.ts +10 -0
- package/src/testing/glsp-harness.ts +422 -0
- package/src/testing/index.ts +14 -0
- package/src/testing/make-noop-glsp-logger.ts +66 -0
- package/src/util/index.ts +10 -0
- package/src/util/service-identifier.ts +49 -0
- package/src/validation/diagnostic-markers.ts +93 -0
- package/src/validation/hydranium-glsp-model-validator.ts +105 -0
- package/src/validation/index.ts +11 -0
|
@@ -0,0 +1,52 @@
|
|
|
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
|
+
* How `HydraniumGlspStorage.saveSourceModel` reacts when a concurrent edit has
|
|
12
|
+
* advanced the document past the version this diagram was last captured at.
|
|
13
|
+
*
|
|
14
|
+
* A save spans otherwise-independent choices — whether to guard on the captured
|
|
15
|
+
* based-on version, whether the GLSP save action awaits the persist, and what
|
|
16
|
+
* happens on failure. Each arm bundles those into one coherent, nameable policy
|
|
17
|
+
* so an adopter selects a whole behaviour rather than assembling an incoherent
|
|
18
|
+
* mix: fire-and-forget plus rethrow would leave an unhandled rejection. It is a
|
|
19
|
+
* *selection of configuration*, delivered as a bound option (see the
|
|
20
|
+
* {@link SaveConflictPolicy} symbol) rather than a behaviour hook.
|
|
21
|
+
*/
|
|
22
|
+
export type SaveConflictPolicy =
|
|
23
|
+
/**
|
|
24
|
+
* Last-write-wins: no based-on guard, await the persist, propagate any
|
|
25
|
+
* failure to the GLSP save action. The default; correct for a single-editor
|
|
26
|
+
* head where nothing races the save.
|
|
27
|
+
*/
|
|
28
|
+
| { kind: 'overwrite' }
|
|
29
|
+
/**
|
|
30
|
+
* Guard on the captured version, await the persist, and surface a
|
|
31
|
+
* `ConflictError` (and any other failure) to the GLSP save action.
|
|
32
|
+
*/
|
|
33
|
+
| { kind: 'reject' }
|
|
34
|
+
/**
|
|
35
|
+
* Guard on the captured version, fire-and-forget, and log + swallow every
|
|
36
|
+
* failure. Correct when another editor of the same document may already have
|
|
37
|
+
* written the truth, so a stale diagram save is benign and must neither
|
|
38
|
+
* block the action nor surface as an error.
|
|
39
|
+
*/
|
|
40
|
+
| { kind: 'drop-and-log' };
|
|
41
|
+
|
|
42
|
+
/** The policy applied when no {@link SaveConflictPolicy} option is bound. */
|
|
43
|
+
export const DEFAULT_SAVE_CONFLICT_POLICY: SaveConflictPolicy = { kind: 'overwrite' };
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* DI token for the {@link SaveConflictPolicy} option. Bind a constant value in a
|
|
47
|
+
* `DiagramModule` to select a non-default policy; left unbound,
|
|
48
|
+
* `HydraniumGlspStorage` falls back to
|
|
49
|
+
* {@link DEFAULT_SAVE_CONFLICT_POLICY}. Shares its name with the type, the way a
|
|
50
|
+
* `class` is both a value and a type.
|
|
51
|
+
*/
|
|
52
|
+
export const SaveConflictPolicy = Symbol('SaveConflictPolicy');
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2024-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 {
|
|
11
|
+
type Action,
|
|
12
|
+
type DirtyStateChangeReason,
|
|
13
|
+
type GModelRootSchema,
|
|
14
|
+
type LayoutOperation,
|
|
15
|
+
ModelState,
|
|
16
|
+
ModelSubmissionHandler,
|
|
17
|
+
SetDirtyStateAction,
|
|
18
|
+
type SetModelAction
|
|
19
|
+
} from '@eclipse-glsp/server';
|
|
20
|
+
import { IntegrityService } from '@hydranium/core';
|
|
21
|
+
import { inject, injectable } from 'inversify';
|
|
22
|
+
import { type AstNode, type DocumentState } from '@hydranium/langium';
|
|
23
|
+
import { type AbstractHydraniumGlspState } from '../state/abstract-hydranium-glsp-state.js';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* GLSP {@link ModelSubmissionHandler} base shared by all hydranium adopters.
|
|
27
|
+
* Wraps {@link submitModel} with an optional document-state ready-gate and
|
|
28
|
+
* post-action observability logging; preserves the {@link createSetModeAction}
|
|
29
|
+
* handshake log; exposes a {@link hasPendingInitialRequest} guard adopters
|
|
30
|
+
* use to suppress external rebuild reactions while the initial-load
|
|
31
|
+
* `RequestModelAction` is still in flight.
|
|
32
|
+
*
|
|
33
|
+
* **What stays adopter-side.** Translation of the source AST to a GModel is
|
|
34
|
+
* owned by GLSP's existing `GModelFactory` interface, bound separately
|
|
35
|
+
* per `DiagramModule`. The submission handler is strictly orchestration —
|
|
36
|
+
* deliberately no `translateToGModel` hook here; adopters wire their per-
|
|
37
|
+
* diagram-type `GModelFactory` subclass via `bindGModelFactory()`.
|
|
38
|
+
*
|
|
39
|
+
* **Hooks.**
|
|
40
|
+
* - {@link readyEvent} — {@link DocumentState} the {@link submitModel} flow
|
|
41
|
+
* awaits via {@link AbstractHydraniumGlspState.ready} before delegating to
|
|
42
|
+
* `super.submitModel`. Default {@link IntegrityService.SettledState}
|
|
43
|
+
* (`IndexedReferences`) — the GModel-factory walk sees a fully linked AST
|
|
44
|
+
* with cross-document references resolved AND post-build integrity rules
|
|
45
|
+
* applied. Adopters that need a different phase (earlier `Linked` for
|
|
46
|
+
* simpler models, later `Validated` to consume diagnostics, or `undefined`
|
|
47
|
+
* to opt out entirely) override this field.
|
|
48
|
+
* - {@link formatSourceRoot} — adopter-specific AST description used in
|
|
49
|
+
* the submit-log line. Default returns `root.$type`.
|
|
50
|
+
*
|
|
51
|
+
* Logs through {@link AbstractHydraniumGlspState.logger} (the state's
|
|
52
|
+
* URI-tagged tracer), so it carries no log-label field of its own.
|
|
53
|
+
*/
|
|
54
|
+
@injectable()
|
|
55
|
+
export class HydraniumGlspSubmissionHandler<TRoot extends AstNode, TSourceModel = string> extends ModelSubmissionHandler {
|
|
56
|
+
@inject(ModelState) declare protected modelState: AbstractHydraniumGlspState<TRoot, TSourceModel>;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Document-state gate awaited before {@link submitModel} delegates to
|
|
60
|
+
* `super.submitModel`. Default {@link IntegrityService.SettledState} so the
|
|
61
|
+
* GModel factory walks a fully-linked, integrity-settled AST out of the
|
|
62
|
+
* box. Adopters override (including to `undefined` to opt out entirely)
|
|
63
|
+
* when their submission flow doesn't need to wait.
|
|
64
|
+
*/
|
|
65
|
+
protected readyEvent: DocumentState | undefined = IntegrityService.SettledState;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* True when a `RequestModelAction` has been accepted but the
|
|
69
|
+
* corresponding {@link SetModelAction} response has not yet been
|
|
70
|
+
* dispatched. Adopter storage uses this to suppress the "external
|
|
71
|
+
* submit" rebuild reaction during the initial-load handshake: that
|
|
72
|
+
* rebuild reaction would call `submitModel('external')`, which rebuilds
|
|
73
|
+
* the GModel and bumps its revision; once the revision changes, the
|
|
74
|
+
* client's first `computedBounds` (still carrying the old revision) is
|
|
75
|
+
* dropped and the initial `SetModelAction` never fires.
|
|
76
|
+
*/
|
|
77
|
+
hasPendingInitialRequest(): boolean {
|
|
78
|
+
return this.requestModelAction !== undefined;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
override async submitModel(reason?: DirtyStateChangeReason, layout?: LayoutOperation): Promise<Action[]> {
|
|
82
|
+
if (this.readyEvent !== undefined) {
|
|
83
|
+
// Adopters with on-build integrity rules wait so the GModel-factory walk sees
|
|
84
|
+
// a settled AST; otherwise the GModel could include elements the build pass is
|
|
85
|
+
// about to rewrite or remove. ready() also refreshes the captured source root
|
|
86
|
+
// via onReadyRefreshed.
|
|
87
|
+
await this.modelState.ready(this.readyEvent);
|
|
88
|
+
}
|
|
89
|
+
const actions = await super.submitModel(reason, layout);
|
|
90
|
+
this._lastSubmittedSignature = this.signatureOf(actions);
|
|
91
|
+
this.logSubmit(reason, actions);
|
|
92
|
+
return actions;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Content signature of the last submission, whatever its reason.
|
|
97
|
+
*
|
|
98
|
+
* **Tracked across every reason because the CLIENT cannot tell them apart.**
|
|
99
|
+
* An operation submit and a later external rebuild that produce the same
|
|
100
|
+
* graph are the same delivery as far as the canvas is concerned, so a
|
|
101
|
+
* comparison scoped to one reason reports "changed" for a model the client
|
|
102
|
+
* already has. `undefined` until the first submit.
|
|
103
|
+
*/
|
|
104
|
+
get lastSubmittedSignature(): string | undefined {
|
|
105
|
+
return this._lastSubmittedSignature;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
protected _lastSubmittedSignature?: string;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Serialise the model-bearing actions of a submission into a comparable
|
|
112
|
+
* signature.
|
|
113
|
+
*
|
|
114
|
+
* `requestId` is fresh per action and `revision` is a monotonic `GModelRoot`
|
|
115
|
+
* counter bumped on every rebuild, so neither reflects content.
|
|
116
|
+
*
|
|
117
|
+
* **`SetDirtyStateAction` is dropped whole rather than having its `reason`
|
|
118
|
+
* stripped by key, and the distinction is load-bearing.** Measured: an
|
|
119
|
+
* operation submit and the external rebuild echoing it are byte-identical
|
|
120
|
+
* across the entire GModel and differ only in that action's `reason`, so
|
|
121
|
+
* leaving it in defeats the comparison outright. Stripping the KEY instead
|
|
122
|
+
* would reach further than intended — `reason` carries meaning on other
|
|
123
|
+
* actions, `SetMarkersAction` among them — and would quietly weaken any
|
|
124
|
+
* later comparison that includes one. Dropping the action also keeps
|
|
125
|
+
* `isDirty` out, which is correct: dirty state is not part of "is this the
|
|
126
|
+
* same model", and a genuine dirty-to-clean change must not be suppressed as
|
|
127
|
+
* a duplicate model.
|
|
128
|
+
*/
|
|
129
|
+
protected signatureOf(actions: Action[]): string {
|
|
130
|
+
return JSON.stringify(
|
|
131
|
+
actions.filter(action => !SetDirtyStateAction.is(action)),
|
|
132
|
+
(key, value) => (key === 'requestId' || key === 'revision' ? undefined : value)
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Logs the {@link SetModelAction} dispatch so the initial
|
|
138
|
+
* `requestModel → setModel` handshake is observable in the server log
|
|
139
|
+
* alongside `Submit model` and `ComputedBounds accepted`.
|
|
140
|
+
*/
|
|
141
|
+
protected override createSetModeAction(newRoot: GModelRootSchema): SetModelAction {
|
|
142
|
+
const action = super.createSetModeAction(newRoot);
|
|
143
|
+
this.modelState.logger.info(
|
|
144
|
+
`Dispatching SetModelAction (response to requestModel #${action.responseId || '?'}) — initial model handshake complete`
|
|
145
|
+
);
|
|
146
|
+
return action;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
protected logSubmit(reason: DirtyStateChangeReason | undefined, actions: Action[]): void {
|
|
150
|
+
const root = this.modelState.root;
|
|
151
|
+
const gChildren = root?.children?.length ?? 0;
|
|
152
|
+
const sourceRoot = this.modelState.sourceRoot;
|
|
153
|
+
const kinds = actions.map(a => a.kind).join(',');
|
|
154
|
+
this.modelState.logger.info(
|
|
155
|
+
`Submit model [reason=${reason ?? 'initial'}, actions=[${kinds}], ` +
|
|
156
|
+
`gmodel={type=${root?.type ?? 'none'}, children=${gChildren}}, ` +
|
|
157
|
+
`ast={${this.formatSourceRoot(sourceRoot)}}]`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Hook for adopter-specific AST description in the submit-log line.
|
|
163
|
+
* Default returns `root.$type` (or `'none'` when the source root is
|
|
164
|
+
* undefined). Adopters with multi-shape source roots override to add
|
|
165
|
+
* per-shape detail.
|
|
166
|
+
*/
|
|
167
|
+
protected formatSourceRoot(root: TRoot | undefined): string {
|
|
168
|
+
return root?.$type ?? 'none';
|
|
169
|
+
}
|
|
170
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 CrossBreeze, EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the MIT License which is available in the project root.
|
|
6
|
+
*
|
|
7
|
+
* SPDX-License-Identifier: MIT
|
|
8
|
+
********************************************************************************/
|
|
9
|
+
|
|
10
|
+
export * from './hydranium-glsp-submission-handler.js';
|
|
@@ -0,0 +1,422 @@
|
|
|
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 'reflect-metadata';
|
|
11
|
+
import {
|
|
12
|
+
type Action,
|
|
13
|
+
type ActionMessage,
|
|
14
|
+
ClientSessionManager,
|
|
15
|
+
EndProgressAction,
|
|
16
|
+
GLSPClientProxy,
|
|
17
|
+
GLSPServer,
|
|
18
|
+
ModelState,
|
|
19
|
+
RequestBoundsAction,
|
|
20
|
+
RequestModelAction,
|
|
21
|
+
SOURCE_URI_ARG,
|
|
22
|
+
SetDirtyStateAction,
|
|
23
|
+
SetModelAction,
|
|
24
|
+
StartProgressAction,
|
|
25
|
+
StatusAction,
|
|
26
|
+
UpdateModelAction,
|
|
27
|
+
UpdateProgressAction
|
|
28
|
+
} from '@eclipse-glsp/server';
|
|
29
|
+
import {
|
|
30
|
+
DefaultGLSPServer,
|
|
31
|
+
InjectionContainer,
|
|
32
|
+
Logger as GlspLogger,
|
|
33
|
+
LoggerFactory,
|
|
34
|
+
NodeActionDispatchScope,
|
|
35
|
+
type ServerModule,
|
|
36
|
+
getRequestParentName
|
|
37
|
+
} from '@eclipse-glsp/server/node.js';
|
|
38
|
+
import { ActionDispatchScope } from '@eclipse-glsp/server';
|
|
39
|
+
import { Container, ContainerModule, type interfaces } from 'inversify';
|
|
40
|
+
import type { AstNode } from '@hydranium/langium';
|
|
41
|
+
import type { ServerSharedServices } from '@hydranium/core';
|
|
42
|
+
import type { Harness } from '@hydranium/protocol/testing';
|
|
43
|
+
import type { AbstractHydraniumGlspState } from '../state/abstract-hydranium-glsp-state.js';
|
|
44
|
+
import { HydraniumTypes } from '../state/hydranium-shared-core-services.js';
|
|
45
|
+
import { createGlspServerOverrides } from '../launcher/glsp-server-overrides.js';
|
|
46
|
+
import { makeNoopGlspLogger } from './make-noop-glsp-logger.js';
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Options for {@link makeGlspHarness}.
|
|
50
|
+
*
|
|
51
|
+
* The harness drives a REAL {@link GLSPServer} in-process: it composes the
|
|
52
|
+
* server container the way `startGlspServer` does (framework default app
|
|
53
|
+
* module + adopter `appModules` + the adopter `serverModule`), but binds
|
|
54
|
+
* {@link GLSPClientProxy} to a capturing stub instead of a socket-backed
|
|
55
|
+
* proxy, so every action the server pushes to the client lands in the
|
|
56
|
+
* harness's `actions` capture. There is no OS socket and no JSON
|
|
57
|
+
* serialization, so nothing here covers the wire layer.
|
|
58
|
+
*
|
|
59
|
+
* `serverModule` + `diagramType` are required; everything else has a
|
|
60
|
+
* test-friendly default. The adopter's shared services / id provider /
|
|
61
|
+
* conflict resolver come from `appModules` (typically an
|
|
62
|
+
* `HydraniumGlspAppModule` subclass) — exactly as in production, so
|
|
63
|
+
* the harness binds none of them itself.
|
|
64
|
+
*/
|
|
65
|
+
export interface MakeGlspHarnessOptions {
|
|
66
|
+
/** Adopter {@link ServerModule} with its diagram module(s) pre-configured via `configureDiagramModule`. */
|
|
67
|
+
readonly serverModule: ServerModule;
|
|
68
|
+
/** Diagram type id passed to `initializeClientSession`; must match the configured diagram module. */
|
|
69
|
+
readonly diagramType: string;
|
|
70
|
+
/**
|
|
71
|
+
* Adopter Inversify modules loaded AFTER the framework default module
|
|
72
|
+
* (which binds `InjectionContainer`, GLSP's `Logger`, `LoggerFactory` and
|
|
73
|
+
* `ActionDispatchScope`). Carries the adopter's
|
|
74
|
+
* `HydraniumTypes.SharedCoreServices` / `HydraniumTypes.ConflictResolver` /
|
|
75
|
+
* language-services bindings. Per-language providers come from the diagram
|
|
76
|
+
* module instead — see `AbstractHydraniumGlspDiagramModule`.
|
|
77
|
+
*/
|
|
78
|
+
readonly appModules?: ReadonlyArray<ContainerModule>;
|
|
79
|
+
/**
|
|
80
|
+
* GLSP logger factory. Default: a silent logger (the harness keeps the
|
|
81
|
+
* test output clean). Override to capture GLSP framework log lines.
|
|
82
|
+
*/
|
|
83
|
+
readonly createLogger?: (caller?: string) => GlspLogger;
|
|
84
|
+
/** `applicationId` for the `initialize` handshake. Default `'test-app'`. */
|
|
85
|
+
readonly applicationId?: string;
|
|
86
|
+
/** Client session id used for `initializeClientSession` + every dispatched `ActionMessage`. Default `'test-session'`. */
|
|
87
|
+
readonly clientSessionId?: string;
|
|
88
|
+
/**
|
|
89
|
+
* Adopter-specific client action kinds to forward IN ADDITION to
|
|
90
|
+
* {@link DEFAULT_CLIENT_ACTION_KINDS}. The server's
|
|
91
|
+
* `ClientActionForwarder` only forwards declared kinds to the capturing
|
|
92
|
+
* `GLSPClientProxy`, so list any custom server→client action a test needs
|
|
93
|
+
* to observe. The standard set covers ordinary round-trips, so most tests
|
|
94
|
+
* leave this unset.
|
|
95
|
+
*/
|
|
96
|
+
readonly additionalClientActionKinds?: ReadonlyArray<string>;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* The standard client-bound action kinds a real GLSP client handles — the
|
|
101
|
+
* minimal set needed for any model round-trip. The harness declares these on
|
|
102
|
+
* the session by default so the capturing {@link GLSPClientProxy} receives
|
|
103
|
+
* them and the action dispatcher never errors with "no handler registered"
|
|
104
|
+
* for an ordinary server→client action. Adopter-specific kinds are added via
|
|
105
|
+
* {@link MakeGlspHarnessOptions.additionalClientActionKinds}.
|
|
106
|
+
*/
|
|
107
|
+
const DEFAULT_CLIENT_ACTION_KINDS: ReadonlyArray<string> = [
|
|
108
|
+
SetModelAction.KIND,
|
|
109
|
+
UpdateModelAction.KIND,
|
|
110
|
+
RequestBoundsAction.KIND,
|
|
111
|
+
SetDirtyStateAction.KIND,
|
|
112
|
+
StatusAction.KIND,
|
|
113
|
+
StartProgressAction.KIND,
|
|
114
|
+
UpdateProgressAction.KIND,
|
|
115
|
+
EndProgressAction.KIND
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
/** Options for {@link GlspHarness.nextModelSubmission}. */
|
|
119
|
+
export interface NextModelSubmissionOptions {
|
|
120
|
+
/** How long to wait. Defaults to 2000ms. */
|
|
121
|
+
readonly timeoutMs?: number;
|
|
122
|
+
/**
|
|
123
|
+
* Reject on timeout (default `true`) or resolve `undefined` (`false`). Pair
|
|
124
|
+
* `false` with a short `timeoutMs` — the wait runs to completion in the
|
|
125
|
+
* passing case.
|
|
126
|
+
*/
|
|
127
|
+
readonly rejectOnTimeout?: boolean;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Bundle returned by {@link makeGlspHarness}. Satisfies the uniform
|
|
132
|
+
* {@link Harness} contract — `state` is the **subject** (the resolved adopter
|
|
133
|
+
* state under test), `server` + `dispatch`/`nextAction` are the **seam** tests
|
|
134
|
+
* drive the action round-trip through, `actions` is the **capture** of every
|
|
135
|
+
* outbound action in arrival order, and `dispose()` is the uniform teardown
|
|
136
|
+
* hook. `state` / `sessionContainer` are only valid after {@link start} (the
|
|
137
|
+
* `ModelState` lives in GLSP's per-session child container, created by
|
|
138
|
+
* `initializeClientSession`).
|
|
139
|
+
*/
|
|
140
|
+
export interface GlspHarness<TState extends AbstractHydraniumGlspState<AstNode, unknown>> extends Harness {
|
|
141
|
+
/** The real GLSP server under test. */
|
|
142
|
+
readonly server: GLSPServer;
|
|
143
|
+
/** The main (app + server) container. */
|
|
144
|
+
readonly container: interfaces.Container;
|
|
145
|
+
/** The per-session child container `initializeClientSession` created. Throws if read before {@link start}. */
|
|
146
|
+
readonly sessionContainer: interfaces.Container;
|
|
147
|
+
/** The adopter `ModelState` resolved from {@link sessionContainer}. Throws if read before {@link start}. */
|
|
148
|
+
readonly state: TState;
|
|
149
|
+
/** Every action the server pushed to the client, in arrival order. Never cleared by the harness. */
|
|
150
|
+
readonly actions: ReadonlyArray<Action>;
|
|
151
|
+
|
|
152
|
+
/** Drive `initialize` then `initializeClientSession`; resolve once the session container exists. */
|
|
153
|
+
start(): Promise<void>;
|
|
154
|
+
/** Drive `server.shutdown()` (disposes client sessions). */
|
|
155
|
+
shutdown(): Promise<void>;
|
|
156
|
+
/** Send an action to the server as `{ clientId, action }`. Fire-and-forget (GLSP `process` is `void`). */
|
|
157
|
+
dispatch(action: Action): void;
|
|
158
|
+
/**
|
|
159
|
+
* Resolve with the next captured action whose `kind` matches — an
|
|
160
|
+
* already-captured-but-unconsumed match resolves immediately, otherwise
|
|
161
|
+
* waits for the next arrival. Rejects after `timeoutMs` (default 2000) so
|
|
162
|
+
* a missing action fails fast instead of hanging.
|
|
163
|
+
*/
|
|
164
|
+
nextAction<T extends Action = Action>(kind: string, timeoutMs?: number): Promise<T>;
|
|
165
|
+
/**
|
|
166
|
+
* FAITHFUL fidelity: open `sourceUri` the way a client does — dispatch
|
|
167
|
+
* `RequestModelAction` with `SOURCE_URI_ARG` — and resolve with the model
|
|
168
|
+
* the server publishes in response.
|
|
169
|
+
*
|
|
170
|
+
* Resolves on **whichever** submission action the diagram produces, because
|
|
171
|
+
* that depends on its `DiagramConfiguration` rather than on the test:
|
|
172
|
+
* `SetModelAction` for a server-laid-out diagram, `RequestBoundsAction` for
|
|
173
|
+
* a client-laid-out one. Awaiting the wrong one is a 2s timeout that names
|
|
174
|
+
* the wrong subsystem, and a test should not have to know which applies.
|
|
175
|
+
*
|
|
176
|
+
* Rejects if nothing is published within `timeoutMs`. Valid only after
|
|
177
|
+
* {@link start}.
|
|
178
|
+
*/
|
|
179
|
+
openDocument(sourceUri: string, timeoutMs?: number): Promise<Action>;
|
|
180
|
+
/**
|
|
181
|
+
* Resolve with the next action that publishes a model — the settling point
|
|
182
|
+
* after an operation, since a successful operation re-submits.
|
|
183
|
+
*
|
|
184
|
+
* Kind-agnostic for the same reason as {@link openDocument}. Set
|
|
185
|
+
* `rejectOnTimeout: false` to ask whether a submission happened *at all*.
|
|
186
|
+
*/
|
|
187
|
+
nextModelSubmission(options?: NextModelSubmissionOptions): Promise<Action | undefined>;
|
|
188
|
+
/**
|
|
189
|
+
* LIGHT fidelity: seed the state's source root directly via
|
|
190
|
+
* `setSourceRoot`, bypassing source-model storage. Valid only after
|
|
191
|
+
* {@link start}.
|
|
192
|
+
*/
|
|
193
|
+
seedSourceRoot(uri: string, root: Parameters<TState['setSourceRoot']>[1]): void;
|
|
194
|
+
|
|
195
|
+
/** Idempotent teardown: `server.shutdown()` then `container.unbindAll()`. */
|
|
196
|
+
dispose(): void;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
interface PendingWaiter {
|
|
200
|
+
/** Kinds this waiter accepts; the first arrival matching any of them wins. */
|
|
201
|
+
readonly kinds: ReadonlyArray<string>;
|
|
202
|
+
readonly resolve: (action: Action | undefined) => void;
|
|
203
|
+
timer: ReturnType<typeof setTimeout>;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* The actions that mean "the server published a model".
|
|
208
|
+
*
|
|
209
|
+
* Which one arrives depends on the diagram's `DiagramConfiguration`, not on
|
|
210
|
+
* what the test did: a server-laid-out diagram gets `SetModelAction`, a
|
|
211
|
+
* client-laid-out one (`needsClientLayout`) gets `RequestBoundsAction`, and a
|
|
212
|
+
* re-submit after an operation may be `UpdateModelAction`. Tests that only care
|
|
213
|
+
* *that* the model was published should not have to encode that choice — see
|
|
214
|
+
* {@link GlspHarness.nextModelSubmission}.
|
|
215
|
+
*
|
|
216
|
+
* Exported for the tests that must COUNT submissions rather than await the next
|
|
217
|
+
* one (asserting that nothing further was published, say). Re-listing the kinds
|
|
218
|
+
* in such a test is a false-green shape: a kind added here would leave the copy
|
|
219
|
+
* silently under-counting.
|
|
220
|
+
*/
|
|
221
|
+
export const MODEL_SUBMISSION_KINDS: ReadonlyArray<string> = [SetModelAction.KIND, UpdateModelAction.KIND, RequestBoundsAction.KIND];
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Wire a real {@link GLSPServer} in-process and drive a GLSP action
|
|
225
|
+
* round-trip against it — dispatch an action → operation/request handler →
|
|
226
|
+
* GModel mutation → response action, captured via a stub
|
|
227
|
+
* {@link GLSPClientProxy}.
|
|
228
|
+
*
|
|
229
|
+
* The harness composes ONE container, collapsing production's parent
|
|
230
|
+
* app-container / per-connection child split, which exists only to share the
|
|
231
|
+
* app container across socket connections.
|
|
232
|
+
*/
|
|
233
|
+
export function makeGlspHarness<TState extends AbstractHydraniumGlspState<AstNode, unknown>>(
|
|
234
|
+
options: MakeGlspHarnessOptions
|
|
235
|
+
): GlspHarness<TState> {
|
|
236
|
+
const applicationId = options.applicationId ?? 'test-app';
|
|
237
|
+
const clientSessionId = options.clientSessionId ?? 'test-session';
|
|
238
|
+
const clientActionKinds = [...DEFAULT_CLIENT_ACTION_KINDS, ...(options.additionalClientActionKinds ?? [])];
|
|
239
|
+
const createLogger = options.createLogger ?? (() => makeNoopGlspLogger());
|
|
240
|
+
|
|
241
|
+
const actions: Action[] = [];
|
|
242
|
+
const consumed = new WeakSet<Action>();
|
|
243
|
+
const waiters: PendingWaiter[] = [];
|
|
244
|
+
|
|
245
|
+
const captureProxy: GLSPClientProxy = {
|
|
246
|
+
process(message: ActionMessage): void {
|
|
247
|
+
const { action } = message;
|
|
248
|
+
actions.push(action);
|
|
249
|
+
const index = waiters.findIndex(waiter => waiter.kinds.includes(action.kind));
|
|
250
|
+
if (index >= 0) {
|
|
251
|
+
const [waiter] = waiters.splice(index, 1);
|
|
252
|
+
consumed.add(action);
|
|
253
|
+
clearTimeout(waiter.timer);
|
|
254
|
+
waiter.resolve(action);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Resolve with the first unconsumed action matching any of `kinds` — one
|
|
261
|
+
* already captured resolves immediately, otherwise the next arrival wins.
|
|
262
|
+
*
|
|
263
|
+
* `rejectOnTimeout` is what lets a caller distinguish the two questions a
|
|
264
|
+
* test asks. `true` (the default) is "this must happen", and a timeout is a
|
|
265
|
+
* failure naming the kinds. `false` is "did this happen?", resolving
|
|
266
|
+
* `undefined` — needed because a **rejected** operation produces no action
|
|
267
|
+
* at all, so the only observable is the absence of one. Mirrors GLSP's own
|
|
268
|
+
* `ActionDispatcher.requestUntil(action, timeoutMs, rejectOnTimeout)`.
|
|
269
|
+
*/
|
|
270
|
+
function waitFor(kinds: ReadonlyArray<string>, timeoutMs: number, rejectOnTimeout: boolean): Promise<Action | undefined> {
|
|
271
|
+
const existing = actions.find(action => kinds.includes(action.kind) && !consumed.has(action));
|
|
272
|
+
if (existing) {
|
|
273
|
+
consumed.add(existing);
|
|
274
|
+
return Promise.resolve(existing);
|
|
275
|
+
}
|
|
276
|
+
return new Promise<Action | undefined>((resolve, reject) => {
|
|
277
|
+
const waiter: PendingWaiter = {
|
|
278
|
+
kinds,
|
|
279
|
+
resolve,
|
|
280
|
+
timer: setTimeout(() => {
|
|
281
|
+
const index = waiters.indexOf(waiter);
|
|
282
|
+
if (index >= 0) {
|
|
283
|
+
waiters.splice(index, 1);
|
|
284
|
+
}
|
|
285
|
+
if (rejectOnTimeout) {
|
|
286
|
+
// Name what DID arrive. A bare "no X within 2000ms" reads as a
|
|
287
|
+
// hang and points at the transport, when the usual cause is an
|
|
288
|
+
// operation handler that threw or declined: the server then
|
|
289
|
+
// emits nothing, or emits only a status/dirty-state action.
|
|
290
|
+
// Distinguishing "nothing happened" from "something else
|
|
291
|
+
// happened" is the difference between a five-minute hunt and a
|
|
292
|
+
// one-line diagnosis.
|
|
293
|
+
const seen = actions.map(captured => captured.kind);
|
|
294
|
+
const context = seen.length === 0 ? 'no actions were captured at all' : `captured since start: ${seen.join(', ')}`;
|
|
295
|
+
reject(
|
|
296
|
+
new Error(
|
|
297
|
+
`makeGlspHarness: no ${kinds.map(kind => `'${kind}'`).join(' / ')} action within ${timeoutMs}ms — ${context}`
|
|
298
|
+
)
|
|
299
|
+
);
|
|
300
|
+
} else {
|
|
301
|
+
resolve(undefined);
|
|
302
|
+
}
|
|
303
|
+
}, timeoutMs)
|
|
304
|
+
};
|
|
305
|
+
waiters.push(waiter);
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const defaultAppModule = new ContainerModule(bind => {
|
|
310
|
+
bind(InjectionContainer).toDynamicValue(ctx => ctx.container);
|
|
311
|
+
bind(GlspLogger).toDynamicValue(ctx => createLogger(getRequestParentName(ctx)));
|
|
312
|
+
bind(LoggerFactory).toFactory(() => (caller: string) => createLogger(caller));
|
|
313
|
+
// `DefaultActionDispatcher` injects this to tell a reentrant dispatch (from
|
|
314
|
+
// inside a running handler, which must run inline) from an external one
|
|
315
|
+
// (which queues). Mirrors GLSP's own `createAppModule`; without it every
|
|
316
|
+
// dispatch throws on `dispatchScope.isReentrant`.
|
|
317
|
+
bind(ActionDispatchScope).to(NodeActionDispatchScope).inSingletonScope();
|
|
318
|
+
bind(HydraniumTypes.Tracer).toDynamicValue(ctx => {
|
|
319
|
+
const tracer = ctx.container.get<ServerSharedServices>(HydraniumTypes.SharedCoreServices).Tracer;
|
|
320
|
+
const caller = getRequestParentName(ctx);
|
|
321
|
+
return caller ? tracer.for(caller) : tracer;
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
const captureProxyModule = new ContainerModule(bind => {
|
|
325
|
+
bind(GLSPClientProxy).toConstantValue(captureProxy);
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
const container = new Container();
|
|
329
|
+
container.load(defaultAppModule, ...(options.appModules ?? []), captureProxyModule);
|
|
330
|
+
container.load(options.serverModule);
|
|
331
|
+
// After the serverModule, which binds `GLSPServer`: the override rebinds that
|
|
332
|
+
// symbol, so it needs the binding to exist. The launchers reach the same tier
|
|
333
|
+
// by passing this to `configure` — a harness that skipped it would answer
|
|
334
|
+
// request failures differently from every real bringup.
|
|
335
|
+
container.load(createGlspServerOverrides());
|
|
336
|
+
const server = container.get<GLSPServer>(GLSPServer);
|
|
337
|
+
|
|
338
|
+
let sessionContainer: interfaces.Container | undefined;
|
|
339
|
+
let state: TState | undefined;
|
|
340
|
+
let disposed = false;
|
|
341
|
+
|
|
342
|
+
return {
|
|
343
|
+
server,
|
|
344
|
+
container,
|
|
345
|
+
actions,
|
|
346
|
+
get sessionContainer(): interfaces.Container {
|
|
347
|
+
if (!sessionContainer) {
|
|
348
|
+
throw new Error('makeGlspHarness: sessionContainer is only available after start()');
|
|
349
|
+
}
|
|
350
|
+
return sessionContainer;
|
|
351
|
+
},
|
|
352
|
+
get state(): TState {
|
|
353
|
+
if (!state) {
|
|
354
|
+
throw new Error('makeGlspHarness: state is only available after start()');
|
|
355
|
+
}
|
|
356
|
+
return state;
|
|
357
|
+
},
|
|
358
|
+
|
|
359
|
+
async start(): Promise<void> {
|
|
360
|
+
await server.initialize({ applicationId, protocolVersion: DefaultGLSPServer.PROTOCOL_VERSION });
|
|
361
|
+
await server.initializeClientSession({ clientSessionId, diagramType: options.diagramType, clientActionKinds });
|
|
362
|
+
const session = container.get<ClientSessionManager>(ClientSessionManager).getSession(clientSessionId);
|
|
363
|
+
if (!session) {
|
|
364
|
+
throw new Error(`makeGlspHarness: no client session '${clientSessionId}' after initializeClientSession`);
|
|
365
|
+
}
|
|
366
|
+
// `session.container` is inversify's concrete `Container`, a nominal type via its
|
|
367
|
+
// private fields — so it clashes across inversify's CJS/ESM dual-package
|
|
368
|
+
// declarations. Typing the harness's container fields as the structural
|
|
369
|
+
// `interfaces.Container` reconciles them with no cast (interfaces compare
|
|
370
|
+
// structurally; the concrete `Container` is only needed for `new Container()`).
|
|
371
|
+
const resolved = session.container;
|
|
372
|
+
sessionContainer = resolved;
|
|
373
|
+
state = resolved.get<TState>(ModelState);
|
|
374
|
+
},
|
|
375
|
+
|
|
376
|
+
async shutdown(): Promise<void> {
|
|
377
|
+
server.shutdown();
|
|
378
|
+
},
|
|
379
|
+
|
|
380
|
+
dispatch(action: Action): void {
|
|
381
|
+
server.process({ clientId: clientSessionId, action });
|
|
382
|
+
},
|
|
383
|
+
|
|
384
|
+
nextAction<T extends Action = Action>(kind: string, timeoutMs = 2000): Promise<T> {
|
|
385
|
+
return waitFor([kind], timeoutMs, true) as Promise<T>;
|
|
386
|
+
},
|
|
387
|
+
|
|
388
|
+
async openDocument(sourceUri: string, timeoutMs = 2000): Promise<Action> {
|
|
389
|
+
if (!state) {
|
|
390
|
+
throw new Error('makeGlspHarness: openDocument() is only valid after start()');
|
|
391
|
+
}
|
|
392
|
+
server.process({
|
|
393
|
+
clientId: clientSessionId,
|
|
394
|
+
action: RequestModelAction.create({ options: { [SOURCE_URI_ARG]: sourceUri } })
|
|
395
|
+
});
|
|
396
|
+
const submission = await waitFor(MODEL_SUBMISSION_KINDS, timeoutMs, true);
|
|
397
|
+
// `waitFor` with rejectOnTimeout only resolves with a real action.
|
|
398
|
+
return submission as Action;
|
|
399
|
+
},
|
|
400
|
+
|
|
401
|
+
nextModelSubmission(options: NextModelSubmissionOptions = {}): Promise<Action | undefined> {
|
|
402
|
+
const { timeoutMs = 2000, rejectOnTimeout = true } = options;
|
|
403
|
+
return waitFor(MODEL_SUBMISSION_KINDS, timeoutMs, rejectOnTimeout);
|
|
404
|
+
},
|
|
405
|
+
|
|
406
|
+
seedSourceRoot(uri: string, root: Parameters<TState['setSourceRoot']>[1]): void {
|
|
407
|
+
if (!state) {
|
|
408
|
+
throw new Error('makeGlspHarness: seedSourceRoot() is only valid after start()');
|
|
409
|
+
}
|
|
410
|
+
state.setSourceRoot(uri, root);
|
|
411
|
+
},
|
|
412
|
+
|
|
413
|
+
dispose(): void {
|
|
414
|
+
if (disposed) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
disposed = true;
|
|
418
|
+
server.shutdown();
|
|
419
|
+
container.unbindAll();
|
|
420
|
+
}
|
|
421
|
+
};
|
|
422
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
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
|
+
// Subpath barrel for `@hydranium/glsp-server/testing` — `makeGlspHarness`,
|
|
11
|
+
// which drives a real GLSP server in-process against a capturing client proxy.
|
|
12
|
+
|
|
13
|
+
export * from './glsp-harness.js';
|
|
14
|
+
export * from './make-noop-glsp-logger.js';
|