@hydranium/glsp-client-theia 1.0.0-next.33 → 1.0.0-next.331

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.
Files changed (87) hide show
  1. package/README.md +60 -98
  2. package/lib/browser/client-contribution.d.ts +87 -25
  3. package/lib/browser/client-contribution.d.ts.map +1 -1
  4. package/lib/browser/client-contribution.js +233 -53
  5. package/lib/browser/client-contribution.js.map +1 -1
  6. package/lib/browser/diagram-only-marker-manager.d.ts.map +1 -1
  7. package/lib/browser/diagram-only-marker-manager.js.map +1 -1
  8. package/lib/browser/diagram-widget.d.ts +43 -25
  9. package/lib/browser/diagram-widget.d.ts.map +1 -1
  10. package/lib/browser/diagram-widget.js +76 -26
  11. package/lib/browser/diagram-widget.js.map +1 -1
  12. package/lib/browser/glsp-client-theia-module.d.ts +12 -5
  13. package/lib/browser/glsp-client-theia-module.d.ts.map +1 -1
  14. package/lib/browser/glsp-client-theia-module.js +27 -12
  15. package/lib/browser/glsp-client-theia-module.js.map +1 -1
  16. package/lib/browser/glsp-diagram-manager.d.ts +40 -1
  17. package/lib/browser/glsp-diagram-manager.d.ts.map +1 -1
  18. package/lib/browser/glsp-diagram-manager.js +100 -0
  19. package/lib/browser/glsp-diagram-manager.js.map +1 -1
  20. package/lib/browser/glsp-message-service.d.ts.map +1 -1
  21. package/lib/browser/glsp-message-service.js.map +1 -1
  22. package/lib/browser/glsp-saveable.d.ts +67 -0
  23. package/lib/browser/glsp-saveable.d.ts.map +1 -0
  24. package/lib/browser/glsp-saveable.js +110 -0
  25. package/lib/browser/glsp-saveable.js.map +1 -0
  26. package/lib/browser/glsp-theia-frontend-module.d.ts.map +1 -1
  27. package/lib/browser/glsp-theia-frontend-module.js +4 -1
  28. package/lib/browser/glsp-theia-frontend-module.js.map +1 -1
  29. package/lib/browser/hydranium-glsp-diagram-configuration.d.ts +3 -1
  30. package/lib/browser/hydranium-glsp-diagram-configuration.d.ts.map +1 -1
  31. package/lib/browser/hydranium-glsp-diagram-configuration.js +3 -1
  32. package/lib/browser/hydranium-glsp-diagram-configuration.js.map +1 -1
  33. package/lib/browser/index.d.ts +4 -3
  34. package/lib/browser/index.d.ts.map +1 -1
  35. package/lib/browser/index.js +7 -5
  36. package/lib/browser/index.js.map +1 -1
  37. package/lib/browser/theia-diagram-loader.d.ts +25 -0
  38. package/lib/browser/theia-diagram-loader.d.ts.map +1 -0
  39. package/lib/browser/theia-diagram-loader.js +42 -0
  40. package/lib/browser/theia-diagram-loader.js.map +1 -0
  41. package/lib/browser/window-session.d.ts +57 -0
  42. package/lib/browser/window-session.d.ts.map +1 -0
  43. package/lib/browser/window-session.js +102 -0
  44. package/lib/browser/window-session.js.map +1 -0
  45. package/lib/node/connection-container-module.js.map +1 -1
  46. package/lib/node/glsp-server-connection-handler.d.ts +11 -20
  47. package/lib/node/glsp-server-connection-handler.d.ts.map +1 -1
  48. package/lib/node/glsp-server-connection-handler.js +12 -15
  49. package/lib/node/glsp-server-connection-handler.js.map +1 -1
  50. package/package.json +40 -49
  51. package/src/browser/client-contribution.ts +254 -55
  52. package/src/browser/diagram-widget.ts +80 -28
  53. package/src/browser/glsp-client-theia-module.ts +34 -13
  54. package/src/browser/glsp-diagram-manager.ts +112 -3
  55. package/src/browser/glsp-saveable.ts +111 -0
  56. package/src/browser/glsp-theia-frontend-module.ts +4 -1
  57. package/src/browser/hydranium-glsp-diagram-configuration.ts +3 -1
  58. package/src/browser/index.ts +12 -5
  59. package/src/browser/theia-diagram-loader.ts +31 -0
  60. package/src/browser/window-session.ts +114 -0
  61. package/src/node/glsp-server-connection-handler.ts +12 -24
  62. package/style/diagram-loading.css +2 -5
  63. package/lib/browser/action-dispatcher.d.ts +0 -85
  64. package/lib/browser/action-dispatcher.d.ts.map +0 -1
  65. package/lib/browser/action-dispatcher.js +0 -238
  66. package/lib/browser/action-dispatcher.js.map +0 -1
  67. package/lib/browser/diagram-loader.d.ts +0 -135
  68. package/lib/browser/diagram-loader.d.ts.map +0 -1
  69. package/lib/browser/diagram-loader.js +0 -166
  70. package/lib/browser/diagram-loader.js.map +0 -1
  71. package/lib/browser/hidden-bounds-updater.d.ts +0 -122
  72. package/lib/browser/hidden-bounds-updater.d.ts.map +0 -1
  73. package/lib/browser/hidden-bounds-updater.js +0 -207
  74. package/lib/browser/hidden-bounds-updater.js.map +0 -1
  75. package/lib/testing/bind-recorder.d.ts +0 -67
  76. package/lib/testing/bind-recorder.d.ts.map +0 -1
  77. package/lib/testing/bind-recorder.js +0 -72
  78. package/lib/testing/bind-recorder.js.map +0 -1
  79. package/lib/testing/index.d.ts +0 -10
  80. package/lib/testing/index.d.ts.map +0 -1
  81. package/lib/testing/index.js +0 -30
  82. package/lib/testing/index.js.map +0 -1
  83. package/src/browser/action-dispatcher.ts +0 -257
  84. package/src/browser/diagram-loader.ts +0 -186
  85. package/src/browser/hidden-bounds-updater.ts +0 -212
  86. package/src/testing/bind-recorder.ts +0 -126
  87. package/src/testing/index.ts +0 -15
@@ -1,212 +0,0 @@
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 {
11
- type Action,
12
- type ElementAndRoutingPoints,
13
- type GModelElement,
14
- type GRoutableElement,
15
- GLSPHiddenBoundsUpdater,
16
- type LocalRequestBoundsAction,
17
- Point,
18
- RequestBoundsAction,
19
- ServerAction,
20
- isRoutable
21
- } from '@eclipse-glsp/client';
22
- import { ChannelLogger } from '@hydranium/client-theia/lib/browser';
23
- import { type Clock, SystemClock } from '@hydranium/protocol';
24
- import { inject, injectable } from '@theia/core/shared/inversify';
25
- import { type VNode } from 'snabbdom';
26
-
27
- /** How many scoped element ids the log line names before summarising the rest.
28
- * A scoped request names one element in practice; the cap only stops a
29
- * pathological caller from writing a thousand ids into the channel. */
30
- const SCOPE_ID_CAP = 3;
31
-
32
- /**
33
- * Instruments sprotty's hidden-bounds pass and repairs degenerate routes before
34
- * they go on the wire.
35
- *
36
- * **Why the framework owns this.** Every GLSP head runs the hidden-bounds pass on
37
- * every model update, and it is the one client-side phase that scales with the
38
- * *rendered element count* rather than the model size — labels, compartments and
39
- * edge decorations expand a modest node count into orders of magnitude more DOM
40
- * elements. Without a timing line there is no way to tell a slow measurement pass
41
- * apart from slow edge routing or a slow server round trip; with one,
42
- * `bounds-request` either exonerates the client in a sentence or names the
43
- * dominant cost.
44
- *
45
- * Emits exactly one `trace` line per bounds request — `trace` because the
46
- * frequency matches the per-action traffic `HydraniumGlspActionDispatcher` logs
47
- * there (a bounds request is driven by those actions), so it belongs with them
48
- * rather than above them. The line carries:
49
- *
50
- * - `server` / `local` — whether the request came from the server or from a
51
- * client-local {@link LocalRequestBoundsAction}
52
- * - `scope=<ids>` — the elements a scoped request named; absent when the request
53
- * covers the whole diagram
54
- * - `measured=N` — DOM elements whose bounds were read. Exceeds the scoped id
55
- * count because the base class expands each requested element to its
56
- * descendants
57
- * - `getBBox=Nms` — {@link getBoundsFromDOM} **only**: the layout pass and the
58
- * `ComputedBoundsAction` assembly stay outside it, so the number isolates raw
59
- * measurement cost
60
- *
61
- * Composed from {@link Clock.measure} rather than `Tracer.time`, matching
62
- * `HydraniumDocumentBuilder`'s phase instrumentation. `Tracer.time` is for "tell
63
- * me when this is slow" and is deliberately silent on a fast success, which would
64
- * drop the element count on every normal pass — and forcing it to emit costs a
65
- * paired `[#N start]` line per request. This is a per-occurrence record, so it
66
- * measures and composes its own line.
67
- *
68
- * Bound by `createGlspClientTheiaModule` via `rebind(GLSPHiddenBoundsUpdater)`.
69
- */
70
- @injectable()
71
- export class HydraniumHiddenBoundsUpdater extends GLSPHiddenBoundsUpdater {
72
- @inject(ChannelLogger) protected readonly channel!: ChannelLogger;
73
-
74
- /** Not injected: the browser container binds no {@link Clock}, and this only
75
- * measures — no logic is gated on time, so there is nothing to fake in
76
- * production. Overridden in tests for a deterministic elapsed value. */
77
- protected readonly clock: Clock = new SystemClock();
78
-
79
- /**
80
- * Scope description for the in-flight request, read from the `cause` in
81
- * {@link postUpdate} and consumed by {@link getBoundsFromDOM} — which the base
82
- * class calls with no arguments, so the cause cannot be threaded through.
83
- */
84
- protected currentScope?: string;
85
-
86
- override decorate(vnode: VNode, element: GModelElement): VNode {
87
- super.decorate(vnode, element);
88
- if (isRoutable(element)) {
89
- this.repairLastRoute(element);
90
- }
91
- return vnode;
92
- }
93
-
94
- /**
95
- * Replace the routing entry the base class just pushed when it carries fewer
96
- * than two points, which is not a valid route for any edge.
97
- *
98
- * `calcElementAndRoute` already guards the case where the registered router
99
- * returns *nothing* — it falls back to source + own routing points + target.
100
- * The hole is one step further in: `calcRoute` filters duplicate points at
101
- * `Number.EPSILON`, so while obstacles still carry sprotty's unmeasured-bounds
102
- * sentinel `(100, 100, -1, -1)` the source and target coincide and a valid
103
- * two-point route collapses to a single point. That result is defined, so the
104
- * upstream truthiness guard never fires and the single point ships.
105
- *
106
- * The substitute is deliberately upstream's own fallback shape rather than a
107
- * new invention, so a repaired route is indistinguishable from a route the
108
- * base class would have produced had it noticed. Only already-invalid payloads
109
- * are touched, so a head with a working router is unaffected.
110
- */
111
- protected repairLastRoute(element: GRoutableElement): void {
112
- const addedRoute = this.element2route.pop();
113
- if (addedRoute === undefined) {
114
- return;
115
- }
116
- this.element2route.push(this.hasValidRoute(addedRoute) ? addedRoute : this.toSourceTargetRoute(element));
117
- }
118
-
119
- /** A route needs at least a start and an end point to describe an edge. */
120
- protected hasValidRoute(route: ElementAndRoutingPoints): boolean {
121
- return route.newRoutingPoints !== undefined && route.newRoutingPoints.length >= 2;
122
- }
123
-
124
- /** Upstream's `calcElementAndRoute` no-router fallback: the element's own
125
- * bendpoints framed by its source and target anchor positions. */
126
- protected toSourceTargetRoute(element: GRoutableElement): ElementAndRoutingPoints {
127
- return {
128
- elementId: element.id,
129
- newRoutingPoints: [element.source?.position ?? Point.ORIGIN, ...element.routingPoints, element.target?.position ?? Point.ORIGIN]
130
- };
131
- }
132
-
133
- protected override getBoundsFromDOM(): void {
134
- if (this.currentScope === undefined) {
135
- // Not inside an instrumented `postUpdate` (a subclass calling directly);
136
- // with no cause there is nothing to attribute a measurement to.
137
- super.getBoundsFromDOM();
138
- return;
139
- }
140
- // Read before the pass: the base class clears the map on its way out.
141
- const measured = this.getElement2BoundsData().size;
142
- const { elapsedMs } = this.clock.measure(() => super.getBoundsFromDOM());
143
- this.channel.trace(this.formatBoundsRequest(this.currentScope, measured, elapsedMs));
144
- }
145
-
146
- /** Format the per-request line. Override to change the wording. */
147
- protected formatBoundsRequest(scope: string, measured: number, elapsedMs: number): string {
148
- return `bounds-request[${scope}] measured=${measured} getBBox=${elapsedMs.toFixed(0)}ms`;
149
- }
150
-
151
- override postUpdate(cause?: Action): void {
152
- if (cause?.kind !== RequestBoundsAction.KIND) {
153
- super.postUpdate(cause);
154
- return;
155
- }
156
- this.currentScope = this.formatCause(cause);
157
- try {
158
- super.postUpdate(cause);
159
- } finally {
160
- this.currentScope = undefined;
161
- }
162
- }
163
-
164
- /**
165
- * Scope facts for the log line: where the request came from, and which
166
- * elements it asked for.
167
- *
168
- * Origin is read from {@link ServerAction}, the `__receivedFromServer` marker
169
- * `GLSPModelSource` stamps on inbound actions, rather than from
170
- * `LocalRequestBoundsAction.is`. The latter would answer the same today, but
171
- * only incidentally: its `elementIDs` check is declared optional, so it is
172
- * really just "a bounds request that did not come from the server". Keying on
173
- * the marker states that directly, and keeps an *unscoped* local request from
174
- * being mislabelled `server` if GLSP ever tightens that guard.
175
- */
176
- protected formatCause(cause: Action): string {
177
- const origin = ServerAction.is(cause) ? 'server' : 'local';
178
- const scope = this.formatScope(cause as RequestBoundsAction);
179
- return scope ? `${origin} scope=${scope}` : origin;
180
- }
181
-
182
- /**
183
- * Name the elements a scoped request asked for, or `undefined` when it covers
184
- * the whole diagram.
185
- *
186
- * Deliberately the ids and not their count. A scoped request in practice names
187
- * a single element, so a count is always `1` and says nothing, while the id
188
- * says *which* element was re-measured — and it is the only part of the line
189
- * that distinguishes one such request from the next. It also stops the line
190
- * reading as a contradiction: `scope=node0, measured=6` is coherent (the base
191
- * class expands a requested element to its descendants, so one id measures six
192
- * DOM elements), where `scoped=1, measured=6` invites the reader to wonder
193
- * which number is wrong.
194
- *
195
- * A request naming only the root is treated as unscoped: it measures
196
- * everything, so the id adds nothing over the absence of a `scope` tag.
197
- */
198
- protected formatScope(cause: RequestBoundsAction): string | undefined {
199
- const scopedIds = (cause as LocalRequestBoundsAction).elementIDs;
200
- if (!scopedIds || scopedIds.length === 0) {
201
- return undefined;
202
- }
203
- const named = scopedIds.filter(id => id !== cause.newRoot?.id);
204
- if (named.length === 0) {
205
- return undefined;
206
- }
207
- if (named.length > SCOPE_ID_CAP) {
208
- return `${named.slice(0, SCOPE_ID_CAP).join(',')},+${named.length - SCOPE_ID_CAP} more`;
209
- }
210
- return named.join(',');
211
- }
212
- }
@@ -1,126 +0,0 @@
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 { interfaces } from 'inversify';
11
-
12
- /**
13
- * One captured `bind(...)` or `rebind(...)` call on a {@link BindRecorder}.
14
- *
15
- * `chain` holds the follow-up methods in invocation order, one string per call,
16
- * rendered as `name(arg, …)`. Arguments are stringified inline so assertions
17
- * can compare structure without re-resolving service identifiers.
18
- */
19
- export interface BindRecorderEntry {
20
- readonly kind: 'bind' | 'rebind';
21
- readonly token: unknown;
22
- readonly chain: string[];
23
- }
24
-
25
- /**
26
- * A spy set of Inversify binding hooks that records every call and its chain.
27
- * Hand them to a module-factory under test — they satisfy Inversify's
28
- * `interfaces.Bind` / `interfaces.Rebind` / `interfaces.Unbind` /
29
- * `interfaces.IsBound` shapes at the call site but never resolve real services.
30
- *
31
- * Together they match GLSP's `BindingContext`, so a factory taking the whole
32
- * context is called as `factory(recorder, options)` — the recorder *is*
33
- * structurally a context.
34
- *
35
- * Use {@link captured} to assert which tokens were touched and with what
36
- * fluent chain. {@link find} is a small ergonomic helper for the common
37
- * lookup pattern.
38
- */
39
- export interface BindRecorder {
40
- readonly bind: interfaces.Bind;
41
- readonly rebind: interfaces.Rebind;
42
- readonly unbind: interfaces.Unbind;
43
- readonly isBound: interfaces.IsBound;
44
- readonly captured: readonly BindRecorderEntry[];
45
- /** Convenience lookup — returns the first entry matching both `kind` and `token`, or `undefined`. */
46
- find(kind: 'bind' | 'rebind', token: unknown): BindRecorderEntry | undefined;
47
- }
48
-
49
- /** Options for {@link makeBindRecorder}. */
50
- export interface BindRecorderOptions {
51
- /**
52
- * What `isBound` answers. Defaults to `true` for every token, matching
53
- * production: a head registers its own module *after* the GLSP default
54
- * modules, so the tokens a framework module rebinds are already bound. Pass a
55
- * list to model a container where only those tokens are bound — e.g. to
56
- * exercise a factory's bind-instead-of-rebind fallback.
57
- */
58
- readonly boundTokens?: readonly unknown[];
59
- }
60
-
61
- /** Methods on Inversify's binding builder that the recorder traps verbatim. */
62
- const RECORDED_CHAIN_METHODS = [
63
- 'toSelf',
64
- 'to',
65
- 'toService',
66
- 'toConstantValue',
67
- 'toDynamicValue',
68
- 'toFactory',
69
- 'toFunction',
70
- 'toAutoFactory',
71
- 'toProvider',
72
- 'inSingletonScope',
73
- 'inTransientScope',
74
- 'inRequestScope',
75
- 'whenTargetNamed',
76
- 'whenTargetTagged',
77
- 'whenInjectedInto',
78
- 'onActivation',
79
- 'onDeactivation'
80
- ] as const;
81
-
82
- /**
83
- * Build a {@link BindRecorder} for testing Inversify module factories without a
84
- * real container: hand it to the factory, then assert over {@link BindRecorder.find}.
85
- *
86
- * The `bind` / `rebind` functions return a chained proxy that ignores its
87
- * arguments structurally but records every method invocation as a string.
88
- * Stringification: functions → `<className>`, symbols → `Symbol(desc).toString()`,
89
- * everything else → `String(value)`. This is enough to assert which tokens are
90
- * involved without dragging real service resolution into the test.
91
- */
92
- export function makeBindRecorder(options: BindRecorderOptions = {}): BindRecorder {
93
- const captured: BindRecorderEntry[] = [];
94
-
95
- const stringifyArg = (value: unknown): string => {
96
- if (typeof value === 'function') {
97
- return `<${value.name || 'fn'}>`;
98
- }
99
- if (typeof value === 'symbol') {
100
- return value.toString();
101
- }
102
- return String(value);
103
- };
104
-
105
- const beginEntry = (kind: 'bind' | 'rebind', token: unknown): unknown => {
106
- const entry: BindRecorderEntry = { kind, token, chain: [] };
107
- captured.push(entry);
108
- const proxy: Record<string, (...args: unknown[]) => unknown> = {};
109
- for (const method of RECORDED_CHAIN_METHODS) {
110
- proxy[method] = (...args: unknown[]): unknown => {
111
- entry.chain.push(`${method}(${args.map(stringifyArg).join(', ')})`);
112
- return proxy;
113
- };
114
- }
115
- return proxy;
116
- };
117
-
118
- const bind = ((token: unknown) => beginEntry('bind', token)) as interfaces.Bind;
119
- const rebind = ((token: unknown) => beginEntry('rebind', token)) as interfaces.Rebind;
120
- const unbind = (() => undefined) as unknown as interfaces.Unbind;
121
- const isBound = ((token: unknown) => options.boundTokens?.includes(token) ?? true) as interfaces.IsBound;
122
- const find = (kind: 'bind' | 'rebind', token: unknown): BindRecorderEntry | undefined =>
123
- captured.find(entry => entry.kind === kind && entry.token === token);
124
-
125
- return { bind, rebind, unbind, isBound, captured, find };
126
- }
@@ -1,15 +0,0 @@
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-client-theia/testing` — the
11
- // GLSP-module-specific Inversify test double. The cross-head doubles
12
- // (`makeStubOutputChannelManager`, `makeStubInversifyContext`) live in
13
- // `@hydranium/client-theia/testing`, alongside the logger they double for.
14
-
15
- export * from './bind-recorder';