@eclipse-glsp/server 2.6.0 → 2.7.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.
Files changed (113) hide show
  1. package/lib/browser/di/app-module.d.ts +1 -1
  2. package/lib/browser/di/app-module.d.ts.map +1 -1
  3. package/lib/browser/di/app-module.js +5 -1
  4. package/lib/browser/di/app-module.js.map +1 -1
  5. package/lib/browser/di/browser-action-dispatch-scope.d.ts +39 -0
  6. package/lib/browser/di/browser-action-dispatch-scope.d.ts.map +1 -0
  7. package/lib/browser/di/browser-action-dispatch-scope.js +77 -0
  8. package/lib/browser/di/browser-action-dispatch-scope.js.map +1 -0
  9. package/lib/browser/index.d.ts +2 -1
  10. package/lib/browser/index.d.ts.map +1 -1
  11. package/lib/browser/index.js +2 -1
  12. package/lib/browser/index.js.map +1 -1
  13. package/lib/common/actions/action-dispatcher.d.ts +107 -5
  14. package/lib/common/actions/action-dispatcher.d.ts.map +1 -1
  15. package/lib/common/actions/action-dispatcher.js +205 -26
  16. package/lib/common/actions/action-dispatcher.js.map +1 -1
  17. package/lib/common/actions/client-action-handler.d.ts +1 -1
  18. package/lib/common/actions/client-action-handler.js +2 -2
  19. package/lib/common/actions/client-action-handler.js.map +1 -1
  20. package/lib/common/actions/global-action-provider.d.ts.map +1 -1
  21. package/lib/common/actions/global-action-provider.js +2 -3
  22. package/lib/common/actions/global-action-provider.js.map +1 -1
  23. package/lib/common/di/client-session-module.d.ts +8 -1
  24. package/lib/common/di/client-session-module.d.ts.map +1 -1
  25. package/lib/common/di/client-session-module.js +9 -1
  26. package/lib/common/di/client-session-module.js.map +1 -1
  27. package/lib/common/di/server-module.d.ts +3 -2
  28. package/lib/common/di/server-module.d.ts.map +1 -1
  29. package/lib/common/di/server-module.js +5 -1
  30. package/lib/common/di/server-module.js.map +1 -1
  31. package/lib/common/di/service-identifiers.d.ts +1 -1
  32. package/lib/common/features/clipboard/request-clipboard-data-action-handler.d.ts +1 -1
  33. package/lib/common/features/clipboard/request-clipboard-data-action-handler.js +1 -2
  34. package/lib/common/features/clipboard/request-clipboard-data-action-handler.js.map +1 -1
  35. package/lib/common/features/layout/computed-bounds-action-handler.d.ts +1 -1
  36. package/lib/common/features/model/model-submission-handler.d.ts +1 -1
  37. package/lib/common/features/model/model-submission-handler.js +1 -1
  38. package/lib/common/features/model/request-model-action-handler.d.ts +1 -1
  39. package/lib/common/features/model/request-model-action-handler.js +1 -1
  40. package/lib/common/features/progress/progress-service.js +1 -1
  41. package/lib/common/index.d.ts +3 -1
  42. package/lib/common/index.d.ts.map +1 -1
  43. package/lib/common/index.js +3 -1
  44. package/lib/common/index.js.map +1 -1
  45. package/lib/common/launch/jsonrpc-server-launcher.d.ts +1 -1
  46. package/lib/common/launch/jsonrpc-server-launcher.d.ts.map +1 -1
  47. package/lib/common/launch/jsonrpc-server-launcher.js +4 -1
  48. package/lib/common/launch/jsonrpc-server-launcher.js.map +1 -1
  49. package/lib/common/operations/create-operation-handler.d.ts +8 -2
  50. package/lib/common/operations/create-operation-handler.d.ts.map +1 -1
  51. package/lib/common/operations/create-operation-handler.js +16 -2
  52. package/lib/common/operations/create-operation-handler.js.map +1 -1
  53. package/lib/common/protocol/glsp-server.d.ts +10 -3
  54. package/lib/common/protocol/glsp-server.d.ts.map +1 -1
  55. package/lib/common/protocol/glsp-server.js +58 -6
  56. package/lib/common/protocol/glsp-server.js.map +1 -1
  57. package/lib/common/session/client-session-manager.d.ts +8 -1
  58. package/lib/common/session/client-session-manager.d.ts.map +1 -1
  59. package/lib/common/session/client-session-manager.js +4 -1
  60. package/lib/common/session/client-session-manager.js.map +1 -1
  61. package/lib/common/utils/action-queue.d.ts +58 -0
  62. package/lib/common/utils/action-queue.d.ts.map +1 -0
  63. package/lib/common/utils/action-queue.js +99 -0
  64. package/lib/common/utils/action-queue.js.map +1 -0
  65. package/lib/common/utils/promise-queue.d.ts +5 -1
  66. package/lib/common/utils/promise-queue.d.ts.map +1 -1
  67. package/lib/common/utils/promise-queue.js +5 -1
  68. package/lib/common/utils/promise-queue.js.map +1 -1
  69. package/lib/node/di/app-module.d.ts +1 -1
  70. package/lib/node/di/app-module.d.ts.map +1 -1
  71. package/lib/node/di/app-module.js +2 -0
  72. package/lib/node/di/app-module.js.map +1 -1
  73. package/lib/node/di/node-action-dispatch-scope.d.ts +26 -0
  74. package/lib/node/di/node-action-dispatch-scope.d.ts.map +1 -0
  75. package/lib/node/di/node-action-dispatch-scope.js +45 -0
  76. package/lib/node/di/node-action-dispatch-scope.js.map +1 -0
  77. package/lib/node/index.d.ts +2 -1
  78. package/lib/node/index.d.ts.map +1 -1
  79. package/lib/node/index.js +2 -1
  80. package/lib/node/index.js.map +1 -1
  81. package/package.json +5 -5
  82. package/src/browser/di/app-module.ts +6 -2
  83. package/src/browser/di/browser-action-dispatch-scope.spec.ts +75 -0
  84. package/src/browser/di/browser-action-dispatch-scope.ts +67 -0
  85. package/src/browser/index.ts +2 -1
  86. package/src/common/actions/action-dispatcher.ts +298 -25
  87. package/src/common/actions/client-action-handler.ts +2 -2
  88. package/src/common/actions/global-action-provider.ts +3 -4
  89. package/src/common/command/recording-command.spec.ts +1 -2
  90. package/src/common/di/client-session-module.ts +9 -1
  91. package/src/common/di/server-module.ts +9 -2
  92. package/src/common/di/service-identifiers.ts +1 -1
  93. package/src/common/features/clipboard/request-clipboard-data-action-handler.ts +2 -2
  94. package/src/common/features/contextactions/context-actions-provider-registry.spec.ts +2 -2
  95. package/src/common/features/layout/computed-bounds-action-handler.ts +1 -1
  96. package/src/common/features/model/model-submission-handler.ts +1 -1
  97. package/src/common/features/model/request-model-action-handler.ts +1 -1
  98. package/src/common/features/progress/progress-service.ts +1 -1
  99. package/src/common/index.ts +3 -1
  100. package/src/common/launch/jsonrpc-server-launcher.ts +4 -1
  101. package/src/common/operations/create-operation-handler.ts +16 -3
  102. package/src/common/protocol/glsp-server.ts +64 -3
  103. package/src/common/session/client-session-manager.ts +12 -1
  104. package/src/common/test/mock-util.ts +21 -3
  105. package/src/common/utils/action-queue.spec.ts +133 -0
  106. package/src/common/utils/action-queue.ts +107 -0
  107. package/src/common/utils/promise-queue.spec.ts +6 -2
  108. package/src/common/utils/promise-queue.ts +5 -1
  109. package/src/node/actions/action-dispatcher.spec.ts +678 -0
  110. package/src/node/di/app-module.ts +4 -2
  111. package/src/node/di/node-action-dispatch-scope.ts +35 -0
  112. package/src/node/index.ts +2 -1
  113. package/src/common/actions/action-dispatcher.spec.ts +0 -291
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2023 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -15,19 +15,21 @@
15
15
  ********************************************************************************/
16
16
  import {
17
17
  Action,
18
+ Deferred,
18
19
  Disposable,
19
20
  MaybeArray,
21
+ RejectAction,
20
22
  RequestAction,
21
23
  ResponseAction,
22
24
  SetModelAction,
23
25
  UpdateModelAction,
24
26
  flatPush
25
27
  } from '@eclipse-glsp/protocol';
26
- import { inject, injectable } from 'inversify';
28
+ import { inject, injectable, postConstruct } from 'inversify';
27
29
  import { ClientId } from '../di/service-identifiers';
30
+ import { ActionQueue } from '../utils/action-queue';
28
31
  import { GLSPServerError } from '../utils/glsp-server-error';
29
32
  import { Logger } from '../utils/logger';
30
- import { PromiseQueue } from '../utils/promise-queue';
31
33
  import { ActionHandler } from './action-handler';
32
34
  import { ActionHandlerRegistry } from './action-handler-registry';
33
35
  import { ClientActionForwarder } from './client-action-handler';
@@ -41,6 +43,8 @@ export const ActionDispatcher = Symbol('ActionDispatcher');
41
43
  export interface ActionDispatcher {
42
44
  /**
43
45
  * Processes the given action by dispatching it to all registered handlers.
46
+ * Responses matching a pending {@link request} short-circuit and resolve that
47
+ * request without being passed to handlers.
44
48
  *
45
49
  * @param action The action that should be dispatched.
46
50
  * @returns A promise indicating when the action processing is complete.
@@ -65,10 +69,92 @@ export interface ActionDispatcher {
65
69
  */
66
70
  dispatchAfterNextUpdate(actions: Action[]): void;
67
71
  dispatchAfterNextUpdate(...actions: Action[]): void;
72
+
73
+ /**
74
+ * Dispatches a request action and returns a promise that resolves when a matching response
75
+ * action is dispatched or rejects if the response is a {@link RejectAction}. The response is
76
+ * _not_ passed to the registered action handlers. Instead, it is the responsibility of the
77
+ * caller of this method to handle the response properly.
78
+ *
79
+ * If the request's `kind` is registered in `ClientActionKinds`, it is forwarded to the client
80
+ * via {@link ClientActionForwarder}. If server-side handlers are registered, they are also
81
+ * executed.
82
+ *
83
+ * Only the first matching response resolves the request. Any additional or late responses
84
+ * are dispatched as normal actions.
85
+ *
86
+ * The promise waits indefinitely until a response arrives or the dispatcher is disposed.
87
+ * Use {@link requestUntil} if a timeout is needed.
88
+ *
89
+ * Note: mutates `action.requestId` (if unset) and `action.timeout`.
90
+ *
91
+ * @param action The request action to dispatch.
92
+ * @returns A promise that resolves with the matching response action.
93
+ */
94
+ request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res>;
95
+
96
+ /**
97
+ * Dispatches a request and waits for a response until the timeout given in `timeoutMs` has
98
+ * been reached. The returned promise is resolved when a response with a matching identifier
99
+ * is dispatched or when the timeout has been reached. That response is _not_ passed to the
100
+ * registered action handlers. Instead, it is the responsibility of the caller of this method
101
+ * to handle the response properly.
102
+ * If `rejectOnTimeout` is set to `false` (default) the returned promise will be resolved with
103
+ * no value, otherwise it will be rejected.
104
+ *
105
+ * Note: mutates `action.requestId` (if unset) and `action.timeout`.
106
+ *
107
+ * @param action The request action to dispatch.
108
+ * @param timeoutMs Maximum wait time in milliseconds. Defaults to
109
+ * {@link RequestAction.timeout} if set, otherwise 2000 ms.
110
+ * @param rejectOnTimeout Whether to reject the promise on timeout.
111
+ * @returns The matching response, or `undefined` on soft timeout.
112
+ */
113
+ requestUntil<Res extends ResponseAction>(
114
+ action: RequestAction<Res>,
115
+ timeoutMs?: number,
116
+ rejectOnTimeout?: boolean
117
+ ): Promise<Res | undefined>;
68
118
  }
69
119
 
120
+ export const ActionDispatchScope = Symbol('ActionDispatchScope');
121
+
122
+ /**
123
+ * Scope marker that lets the {@link ActionDispatcher} know whether a call to `dispatch()`
124
+ * originates from inside a running handler (reentrant) or from outside (external).
125
+ *
126
+ * The {@link DefaultActionDispatcher.processActionQueue} loop wraps each action in {@link enter}
127
+ * so that reentrant `dispatch()` calls (handler responses, injected dispatcher calls) can be
128
+ * recognized via {@link isReentrant} and executed inline instead of being queued.
129
+ *
130
+ * Used by the {@link DefaultActionDispatcher} implementation.
131
+ */
132
+ export interface ActionDispatchScope {
133
+ /**
134
+ * Executes the callback inside the dispatch scope. While the callback (and its full async
135
+ * continuation) is running, {@link isReentrant} returns `true` for reentrant calls.
136
+ */
137
+ enter<R>(callback: () => R): R;
138
+
139
+ /**
140
+ * Returns `true` if the given dispatch is reentrant — i.e. it originates from within a
141
+ * running {@link enter} callback (handler response or injected dispatcher call) and should
142
+ * run inline rather than being queued.
143
+ *
144
+ * Implementations may inspect the action to apply additional guards, e.g. to ensure
145
+ * client-originated actions are always queued regardless of scope state.
146
+ */
147
+ isReentrant(action: Action): boolean;
148
+ }
149
+
150
+ /**
151
+ * Default {@link ActionDispatcher}. External dispatches are queued and processed one at a
152
+ * time; dispatches made from within a running handler run inline with the containing action.
153
+ */
70
154
  @injectable()
71
155
  export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
156
+ protected static readonly STALE_TIMEOUT_GRACE_MS = 30_000;
157
+
72
158
  @inject(ActionHandlerRegistry)
73
159
  protected actionHandlerRegistry: ActionHandlerRegistry;
74
160
 
@@ -81,15 +167,50 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
81
167
  @inject(ClientId)
82
168
  protected clientId: string;
83
169
 
84
- protected actionQueue = new PromiseQueue();
170
+ @inject(ActionDispatchScope)
171
+ protected dispatchScope: ActionDispatchScope;
172
+
173
+ protected actionQueue = new ActionQueue<Action>();
174
+
85
175
  protected postUpdateQueue: Action[] = [];
176
+ protected readonly pendingRequests = new Map<string, Deferred<ResponseAction | undefined>>();
177
+ protected readonly requestTimeouts = new Map<string, NodeJS.Timeout>();
178
+ protected nextRequestId = 1;
179
+
180
+ @postConstruct()
181
+ protected initialize(): void {
182
+ // Fire-and-forget: the loop is meant to run for the dispatcher's lifetime; surface any
183
+ // unexpected termination via the logger instead of an unhandled rejection.
184
+ this.processActionQueue().catch(error => this.logger.error('Action queue processor terminated unexpectedly', error));
185
+ }
186
+
187
+ protected generateRequestId(): string {
188
+ return `server_${this.clientId}_${this.nextRequestId++}`;
189
+ }
86
190
 
87
191
  dispatch(action: Action): Promise<void> {
88
- // Dont queue actions that are just delegated to the client
89
- if (this.clientActionForwarder.shouldForwardToClient(action)) {
192
+ // Intercept first to avoid deadlock: a handler may be awaiting this response.
193
+ if (this.interceptPendingResponse(action)) {
194
+ return Promise.resolve();
195
+ }
196
+ // Reentrant dispatches run inline to preserve ordering with the containing action.
197
+ if (this.dispatchScope.isReentrant(action)) {
90
198
  return this.doDispatch(action);
91
199
  }
92
- return this.actionQueue.enqueue(() => this.doDispatch(action));
200
+ // External dispatches are queued and processed sequentially.
201
+ return this.actionQueue.push(action);
202
+ }
203
+
204
+ protected async processActionQueue(): Promise<void> {
205
+ // Process each action inside the dispatch scope so reentrant dispatch() calls are recognized.
206
+ for await (const entry of this.actionQueue.consume()) {
207
+ try {
208
+ await this.dispatchScope.enter(() => this.doDispatch(entry.item));
209
+ entry.resolve();
210
+ } catch (error) {
211
+ entry.reject(error);
212
+ }
213
+ }
93
214
  }
94
215
 
95
216
  protected async doDispatch(action: Action): Promise<void> {
@@ -98,6 +219,12 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
98
219
 
99
220
  const actionHandlers = this.actionHandlerRegistry.get(action.kind);
100
221
  if (!handledOnClient && actionHandlers.length === 0) {
222
+ // Stale response (e.g. late `RejectAction`) for a pending request that's already
223
+ // gone — protocol signal, not a handler invocation; drop silently.
224
+ if (ResponseAction.is(action)) {
225
+ this.logger.debug(`Stale response '${action.kind}' (responseId='${action.responseId}') has no handler; dropping.`);
226
+ return;
227
+ }
101
228
  throw new GLSPServerError(`No handler registered for action kind: ${action.kind}`);
102
229
  }
103
230
 
@@ -107,35 +234,37 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
107
234
  responses.push(...response);
108
235
  }
109
236
 
110
- if (this.postUpdateQueue.length > 0 && (UpdateModelAction.is(action) || SetModelAction.is(action))) {
111
- responses.push(...this.postUpdateQueue);
112
- this.postUpdateQueue = [];
113
- }
237
+ // Append post-update actions to responses so they are dispatched in the same inline
238
+ // batch as the handler responses, preserving sequential order.
239
+ responses.push(...this.drainPostUpdateQueue(action));
114
240
 
115
241
  await this.dispatchResponses(responses);
116
242
  }
117
243
 
118
- protected async executeHandler(handler: ActionHandler, request: Action): Promise<Action[]> {
119
- const responseActions = await handler.execute(request);
120
- return responseActions.map(action => respond(request, action));
244
+ protected async executeHandler(handler: ActionHandler, action: Action): Promise<Action[]> {
245
+ const responseActions = await handler.execute(action);
246
+ return responseActions.map(response => respond(action, response));
121
247
  }
122
248
 
123
- protected dispatchResponses(actions: Action[]): Promise<void> {
124
- if (actions.length === 0) {
125
- return Promise.resolve();
249
+ protected async dispatchResponses(actions: Action[]): Promise<void> {
250
+ // Sequential dispatch inside the current dispatch scope. Each response goes inline via
251
+ // the reentrant path, or is intercepted if it resolves a pending request().
252
+ for (const action of actions) {
253
+ await this.dispatch(action);
126
254
  }
127
- const responseQueue = new PromiseQueue();
128
- const responses = actions.map(action => responseQueue.enqueue(() => this.doDispatch(action)));
129
- return Promise.all(responses).then(() => Promise.resolve());
130
255
  }
131
256
 
132
- dispatchAll(...actions: MaybeArray<Action>[]): Promise<void> {
257
+ async dispatchAll(...actions: MaybeArray<Action>[]): Promise<void> {
133
258
  if (actions.length === 0) {
134
- return Promise.resolve();
259
+ return;
135
260
  }
136
261
  const flat: Action[] = [];
137
262
  flatPush(flat, actions);
138
- return Promise.all(flat.map(action => this.dispatch(action))).then(() => Promise.resolve());
263
+ // Sequential dispatch: external calls were already FIFO via the queue, but reentrant
264
+ // calls also need deterministic ordering so handlers see each other's effects in order.
265
+ for (const action of flat) {
266
+ await this.dispatch(action);
267
+ }
139
268
  }
140
269
 
141
270
  dispatchAfterNextUpdate(...actions: MaybeArray<Action>[]): void {
@@ -144,8 +273,152 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
144
273
  }
145
274
  }
146
275
 
276
+ request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res> {
277
+ return this.doRequest(action, undefined, true) as Promise<Res>;
278
+ }
279
+
280
+ requestUntil<Res extends ResponseAction>(
281
+ action: RequestAction<Res>,
282
+ timeoutMs: number = action.timeout ?? 2000,
283
+ rejectOnTimeout = false
284
+ ): Promise<Res | undefined> {
285
+ return this.doRequest(action, timeoutMs, rejectOnTimeout);
286
+ }
287
+
288
+ protected doRequest<Res extends ResponseAction>(
289
+ action: RequestAction<Res>,
290
+ timeoutMs: number | undefined,
291
+ rejectOnTimeout: boolean
292
+ ): Promise<Res | undefined> {
293
+ if (!action.requestId || action.requestId === '') {
294
+ action.requestId = this.generateRequestId();
295
+ }
296
+ // Stamp the effective timeout onto the action so the receiving side
297
+ // (handleServerRequest/handleClientRequest) can respect it.
298
+ action.timeout = timeoutMs;
299
+
300
+ const deferred = new Deferred<ResponseAction | undefined>();
301
+ this.pendingRequests.set(action.requestId, deferred);
302
+
303
+ if (timeoutMs !== undefined) {
304
+ const timeout = setTimeout(() => {
305
+ if (this.pendingRequests.delete(action.requestId)) {
306
+ // Keep the requestTimeouts entry briefly as a stale marker so a late response
307
+ // can be filtered, then drop it after a grace period to avoid leaking markers
308
+ // for requests whose late responses never arrive.
309
+ const cleanup = setTimeout(
310
+ () => this.requestTimeouts.delete(action.requestId),
311
+ DefaultActionDispatcher.STALE_TIMEOUT_GRACE_MS
312
+ );
313
+ cleanup.unref?.();
314
+ const message = `Request '${action.requestId}' (${action.kind}) timed out after ${timeoutMs}ms`;
315
+ if (rejectOnTimeout) {
316
+ deferred.reject(new Error(message));
317
+ } else {
318
+ this.logger.info(message);
319
+ deferred.resolve(undefined);
320
+ }
321
+ }
322
+ }, timeoutMs);
323
+
324
+ this.requestTimeouts.set(action.requestId, timeout);
325
+ }
326
+
327
+ // dispatch() routes correctly on its own: external callers queue, handler-internal
328
+ // callers run inline via the ActionDispatchScope. The matching response resolves
329
+ // the deferred out-of-band via interceptPendingResponse().
330
+ const dispatchPromise = this.dispatch(action);
331
+
332
+ dispatchPromise.catch(error => {
333
+ if (this.pendingRequests.delete(action.requestId)) {
334
+ const timeout = this.requestTimeouts.get(action.requestId);
335
+ if (timeout !== undefined) {
336
+ clearTimeout(timeout);
337
+ this.requestTimeouts.delete(action.requestId);
338
+ }
339
+ deferred.reject(error);
340
+ }
341
+ });
342
+
343
+ return deferred.promise as Promise<Res | undefined>;
344
+ }
345
+
346
+ /**
347
+ * If the given action is a {@link SetModelAction} or {@link UpdateModelAction} and there are actions
348
+ * queued via {@link dispatchAfterNextUpdate}, drain and return them.
349
+ *
350
+ * @returns The drained actions, or an empty array if nothing to drain.
351
+ */
352
+ protected drainPostUpdateQueue(action: Action): Action[] {
353
+ if (this.postUpdateQueue.length > 0 && (UpdateModelAction.is(action) || SetModelAction.is(action))) {
354
+ const actions = [...this.postUpdateQueue];
355
+ this.postUpdateQueue = [];
356
+ return actions;
357
+ }
358
+ return [];
359
+ }
360
+
361
+ /**
362
+ * Checks whether the given action is a response matching a pending {@link request} or
363
+ * {@link requestUntil} call. If matched, resolves (or rejects) the corresponding deferred
364
+ * and returns `true` so the caller can short-circuit normal dispatch.
365
+ *
366
+ * For responses with a valid `responseId` but no matching pending request, checks for a stale
367
+ * timeout entry (timed-out request) and clears the `responseId` so the action is not forwarded
368
+ * by {@link ClientActionForwarder}. If no stale entry exists, the `responseId` is left intact
369
+ * for normal forwarding.
370
+ */
371
+ protected interceptPendingResponse(action: Action): boolean {
372
+ if (!ResponseAction.hasValidResponseId(action)) {
373
+ return false;
374
+ }
375
+ const deferred = this.pendingRequests.get(action.responseId);
376
+ if (deferred !== undefined) {
377
+ this.pendingRequests.delete(action.responseId);
378
+ const timeout = this.requestTimeouts.get(action.responseId);
379
+ if (timeout !== undefined) {
380
+ clearTimeout(timeout);
381
+ this.requestTimeouts.delete(action.responseId);
382
+ }
383
+ // Intercepted responses skip doDispatch, so drain post-update actions here when the
384
+ // response is an UpdateModel/SetModel. RejectAction does not trigger a drain; pending
385
+ // post-update actions stay queued until the next successful update.
386
+ const postUpdateActions = this.drainPostUpdateQueue(action);
387
+ if (RejectAction.is(action)) {
388
+ deferred.reject(new Error(`${action.message}${action.detail ? ': ' + action.detail : ''}`));
389
+ } else {
390
+ deferred.resolve(action);
391
+ }
392
+ if (postUpdateActions.length > 0) {
393
+ // Fire-and-forget: callers of request() expect the resolved response, not the
394
+ // unrelated post-update fan-out; awaiting here would couple them unnecessarily.
395
+ this.dispatchResponses(postUpdateActions).catch(error =>
396
+ this.logger.error('Failed to dispatch post-update actions', error)
397
+ );
398
+ }
399
+ return true;
400
+ }
401
+ // Late response for a timed-out request: clear responseId so ClientActionForwarder does
402
+ // not re-emit it to the client.
403
+ const staleTimeout = this.requestTimeouts.get(action.responseId);
404
+ if (staleTimeout !== undefined) {
405
+ clearTimeout(staleTimeout);
406
+ this.requestTimeouts.delete(action.responseId);
407
+ this.logger.debug(`Late response for timed-out request '${action.responseId}', dispatching as normal action`);
408
+ action.responseId = '';
409
+ }
410
+ return false;
411
+ }
412
+
147
413
  dispose(): void {
148
- this.actionQueue.clear();
414
+ // Reject queued actions: no further processing should happen after dispose.
415
+ this.actionQueue.rejectPending(new Error('ActionDispatcher disposed'));
416
+ this.actionQueue.stop();
417
+ this.pendingRequests.forEach((deferred, id) => deferred.reject(new Error(`Request '${id}' cancelled: dispatcher disposed`)));
418
+ this.pendingRequests.clear();
419
+ this.requestTimeouts.forEach(timeout => clearTimeout(timeout));
420
+ this.requestTimeouts.clear();
421
+ this.postUpdateQueue = [];
149
422
  }
150
423
  }
151
424
 
@@ -158,7 +431,7 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
158
431
  */
159
432
  export function respond(request: Action, response: Action): Action {
160
433
  if (RequestAction.is(request) && ResponseAction.is(response)) {
161
- (response as any).responseId = request.requestId;
434
+ response.responseId = request.requestId;
162
435
  }
163
436
  return response;
164
437
  }
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2023 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -54,6 +54,6 @@ export class ClientActionForwarder {
54
54
  if (ClientAction.is(action)) {
55
55
  return false;
56
56
  }
57
- return this.actionKinds.has(action.kind) || ResponseAction.is(action);
57
+ return this.actionKinds.has(action.kind) || ResponseAction.hasValidResponseId(action);
58
58
  }
59
59
  }
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2023 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -15,7 +15,7 @@
15
15
  ********************************************************************************/
16
16
  import { distinctAdd } from '@eclipse-glsp/protocol';
17
17
  import { Container, ContainerModule, inject, injectable } from 'inversify';
18
- import { createClientSessionModule } from '../di/client-session-module';
18
+ import { TEMPORARY_CLIENT_ID, createClientSessionModule } from '../di/client-session-module';
19
19
  import { DiagramModules, InjectionContainer } from '../di/service-identifiers';
20
20
  import { ClientSessionInitializer } from '../session/client-session-initializer';
21
21
  import { ActionHandlerRegistry } from './action-handler-registry';
@@ -50,8 +50,7 @@ export class DefaultGlobalActionProvider implements GlobalActionProvider {
50
50
  createDiagramContainer(serverContainer: Container, modules: ContainerModule[]): Container {
51
51
  const container = serverContainer.createChild();
52
52
  const clientSessionModule = createClientSessionModule({
53
- clientId: 'tempId',
54
- // eslint-disable-next-line @typescript-eslint/no-empty-function
53
+ clientId: TEMPORARY_CLIENT_ID,
55
54
  glspClient: { process: () => {} },
56
55
  clientActionKinds: []
57
56
  });
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2023-2024 EclipseSource and others.
2
+ * Copyright (c) 2023-2026 EclipseSource and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -53,7 +53,6 @@ describe('RecordingCommand', () => {
53
53
  });
54
54
 
55
55
  it('should be undoable after execution', async () => {
56
- // eslint-disable-next-line @typescript-eslint/no-empty-function
57
56
  const command = new TestRecordingCommand(jsonObject, () => {});
58
57
  expect(command.canUndo()).to.be.false;
59
58
  await command.execute();
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2024 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -24,6 +24,14 @@ export interface ClientSessionModuleOptions {
24
24
  clientActionKinds: string[];
25
25
  }
26
26
 
27
+ /**
28
+ * Synthetic `ClientId` used by framework probes that load diagram modules onto a throwaway
29
+ * container — e.g. {@link DefaultGlobalActionProvider} scraping action-kind metadata at server
30
+ * startup. Adopters can detect this id in their `ClientSessionInitializer`s to suppress
31
+ * setup-time diagnostics that don't apply when no real GLSP client is connected.
32
+ */
33
+ export const TEMPORARY_CLIENT_ID = 'tempId';
34
+
27
35
  /**
28
36
  * Creates the DI module that binds client session specific configuration
29
37
  */
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2023 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -13,7 +13,7 @@
13
13
  *
14
14
  * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
15
15
  ********************************************************************************/
16
- import { GLSPServer, GLSPServerListener } from '@eclipse-glsp/protocol';
16
+ import { GLSPServer, GLSPServerInitializer, GLSPServerListener } from '@eclipse-glsp/protocol';
17
17
  import { ContainerModule, interfaces } from 'inversify';
18
18
  import { DefaultGlobalActionProvider, GlobalActionProvider } from '../actions/global-action-provider';
19
19
  import { DefaultGLSPServer } from '../protocol/glsp-server';
@@ -77,6 +77,9 @@ export class ServerModule extends GLSPModule {
77
77
  this.configureMultiBinding(new MultiBinding<GLSPServerListener>(GLSPServerListener), binding =>
78
78
  this.configureGLSPServerListeners(binding)
79
79
  );
80
+ this.configureMultiBinding(new MultiBinding<GLSPServerInitializer>(GLSPServerInitializer), binding =>
81
+ this.configureGLSPServerInitializers(binding)
82
+ );
80
83
 
81
84
  applyBindingTarget(context, GlobalActionProvider, this.bindGlobalActionProvider()).inSingletonScope();
82
85
 
@@ -112,4 +115,8 @@ export class ServerModule extends GLSPModule {
112
115
  protected configureGLSPServerListeners(binding: MultiBinding<GLSPServerListener>): void {
113
116
  binding.add({ service: ClientSessionManager });
114
117
  }
118
+
119
+ protected configureGLSPServerInitializers(binding: MultiBinding<GLSPServerInitializer>): void {
120
+ // Can be overridden to add contributions
121
+ }
115
122
  }
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2023 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2023 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -44,7 +44,7 @@ export class RequestClipboardDataActionHandler implements ActionHandler {
44
44
  selectedElements.forEach(element => {
45
45
  schemas.push(this.modelSerializer.createSchema(element));
46
46
  });
47
- // eslint-disable-next-line no-null/no-null
47
+
48
48
  clipboardData['application/json'] = JSON.stringify(schemas, undefined, 2);
49
49
  return [SetClipboardDataAction.create(clipboardData)];
50
50
  }
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2023 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -15,11 +15,11 @@
15
15
  ********************************************************************************/
16
16
  import { GModelElement } from '@eclipse-glsp/graph';
17
17
  import { Args, LabeledAction, Point } from '@eclipse-glsp/protocol';
18
+ import { expect } from 'chai';
18
19
  import { CommandPaletteActionProvider } from './command-palette-action-provider';
19
20
  import { ContextActionsProvider } from './context-actions-provider';
20
21
  import { ContextActionsProviderRegistry } from './context-actions-provider-registry';
21
22
  import { DefaultToolPaletteItemProvider } from './tool-palette-item-provider';
22
- import { expect } from 'chai';
23
23
 
24
24
  describe('Test DefaultContextActionsProviderRegistry', () => {
25
25
  it('check if default registry is empty', () => {
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2023 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2024 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2025 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2023 EclipseSource and others.
2
+ * Copyright (c) 2023-2026 EclipseSource and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2022-2024 STMicroelectronics and others.
2
+ * Copyright (c) 2022-2026 STMicroelectronics and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -52,6 +52,7 @@ export * from './features/model/model-state';
52
52
  export * from './features/model/model-submission-handler';
53
53
  export * from './features/model/request-model-action-handler';
54
54
  export * from './features/model/save-model-action-handler';
55
+ export * from './features/model/set-edit-mode-action-handler';
55
56
  export * from './features/model/source-model-storage';
56
57
  export * from './features/navigation/json-opener-options';
57
58
  export * from './features/navigation/navigation-target-provider';
@@ -95,6 +96,7 @@ export * from './session/client-session-factory';
95
96
  export * from './session/client-session-initializer';
96
97
  export * from './session/client-session-listener';
97
98
  export * from './session/client-session-manager';
99
+ export * from './utils/action-queue';
98
100
  export * from './utils/args-util';
99
101
  export * from './utils/client-options-util';
100
102
  export * from './utils/console-logger';
@@ -1,5 +1,5 @@
1
1
  /********************************************************************************
2
- * Copyright (c) 2023 EclipseSource and others.
2
+ * Copyright (c) 2023-2026 EclipseSource and others.
3
3
  *
4
4
  * This program and the accompanying materials are made available under the
5
5
  * terms of the Eclipse Public License v. 2.0 which is available at
@@ -83,6 +83,9 @@ export abstract class JsonRpcGLSPServerLauncher<T> extends GLSPServerLauncher<T>
83
83
  serverInstance.clientConnection.onNotification(JsonrpcGLSPClient.ShutdownNotification, () =>
84
84
  this.disposeServerInstance(serverInstance)
85
85
  );
86
+ // A connection may be unceremoniously be closed (e.g., closing/reloading the browser) in which
87
+ // case the server must still be disposed
88
+ serverInstance.clientConnection.onClose(() => this.disposeServerInstance(serverInstance));
86
89
 
87
90
  this.logger.info('Starting GLSP server connection');
88
91
  }