@eclipse-glsp/server 2.6.0 → 2.7.0-next.6

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 (107) 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/di/browser-dispatch-context.d.ts +39 -0
  10. package/lib/browser/di/browser-dispatch-context.d.ts.map +1 -0
  11. package/lib/browser/di/browser-dispatch-context.js +57 -0
  12. package/lib/browser/di/browser-dispatch-context.js.map +1 -0
  13. package/lib/browser/di/sync-dispatch-context.d.ts +39 -0
  14. package/lib/browser/di/sync-dispatch-context.d.ts.map +1 -0
  15. package/lib/browser/di/sync-dispatch-context.js +57 -0
  16. package/lib/browser/di/sync-dispatch-context.js.map +1 -0
  17. package/lib/browser/index.d.ts +2 -1
  18. package/lib/browser/index.d.ts.map +1 -1
  19. package/lib/browser/index.js +2 -1
  20. package/lib/browser/index.js.map +1 -1
  21. package/lib/common/actions/action-dispatcher.d.ts +107 -5
  22. package/lib/common/actions/action-dispatcher.d.ts.map +1 -1
  23. package/lib/common/actions/action-dispatcher.js +199 -26
  24. package/lib/common/actions/action-dispatcher.js.map +1 -1
  25. package/lib/common/actions/client-action-handler.d.ts +1 -1
  26. package/lib/common/actions/client-action-handler.js +2 -2
  27. package/lib/common/actions/client-action-handler.js.map +1 -1
  28. package/lib/common/actions/global-action-provider.js +1 -2
  29. package/lib/common/actions/global-action-provider.js.map +1 -1
  30. package/lib/common/di/service-identifiers.d.ts +1 -1
  31. package/lib/common/features/clipboard/request-clipboard-data-action-handler.d.ts +1 -1
  32. package/lib/common/features/clipboard/request-clipboard-data-action-handler.js +1 -2
  33. package/lib/common/features/clipboard/request-clipboard-data-action-handler.js.map +1 -1
  34. package/lib/common/features/layout/computed-bounds-action-handler.d.ts +1 -1
  35. package/lib/common/features/model/model-submission-handler.d.ts +1 -1
  36. package/lib/common/features/model/model-submission-handler.js +1 -1
  37. package/lib/common/features/model/request-model-action-handler.d.ts +1 -1
  38. package/lib/common/features/model/request-model-action-handler.js +1 -1
  39. package/lib/common/features/progress/progress-service.js +1 -1
  40. package/lib/common/index.d.ts +2 -1
  41. package/lib/common/index.d.ts.map +1 -1
  42. package/lib/common/index.js +2 -1
  43. package/lib/common/index.js.map +1 -1
  44. package/lib/common/protocol/glsp-server.d.ts +4 -2
  45. package/lib/common/protocol/glsp-server.d.ts.map +1 -1
  46. package/lib/common/protocol/glsp-server.js +35 -4
  47. package/lib/common/protocol/glsp-server.js.map +1 -1
  48. package/lib/common/utils/action-channel.d.ts +58 -0
  49. package/lib/common/utils/action-channel.d.ts.map +1 -0
  50. package/lib/common/utils/action-channel.js +99 -0
  51. package/lib/common/utils/action-channel.js.map +1 -0
  52. package/lib/common/utils/action-queue.d.ts +58 -0
  53. package/lib/common/utils/action-queue.d.ts.map +1 -0
  54. package/lib/common/utils/action-queue.js +99 -0
  55. package/lib/common/utils/action-queue.js.map +1 -0
  56. package/lib/common/utils/promise-queue.d.ts +5 -1
  57. package/lib/common/utils/promise-queue.d.ts.map +1 -1
  58. package/lib/common/utils/promise-queue.js +5 -1
  59. package/lib/common/utils/promise-queue.js.map +1 -1
  60. package/lib/node/di/app-module.d.ts +1 -1
  61. package/lib/node/di/app-module.d.ts.map +1 -1
  62. package/lib/node/di/app-module.js +2 -0
  63. package/lib/node/di/app-module.js.map +1 -1
  64. package/lib/node/di/async-dispatch-context.d.ts +26 -0
  65. package/lib/node/di/async-dispatch-context.d.ts.map +1 -0
  66. package/lib/node/di/async-dispatch-context.js +35 -0
  67. package/lib/node/di/async-dispatch-context.js.map +1 -0
  68. package/lib/node/di/node-action-dispatch-scope.d.ts +26 -0
  69. package/lib/node/di/node-action-dispatch-scope.d.ts.map +1 -0
  70. package/lib/node/di/node-action-dispatch-scope.js +45 -0
  71. package/lib/node/di/node-action-dispatch-scope.js.map +1 -0
  72. package/lib/node/di/node-dispatch-context.d.ts +26 -0
  73. package/lib/node/di/node-dispatch-context.d.ts.map +1 -0
  74. package/lib/node/di/node-dispatch-context.js +35 -0
  75. package/lib/node/di/node-dispatch-context.js.map +1 -0
  76. package/lib/node/index.d.ts +2 -1
  77. package/lib/node/index.d.ts.map +1 -1
  78. package/lib/node/index.js +2 -1
  79. package/lib/node/index.js.map +1 -1
  80. package/package.json +5 -5
  81. package/src/browser/di/app-module.ts +6 -2
  82. package/src/browser/di/browser-action-dispatch-scope.spec.ts +75 -0
  83. package/src/browser/di/browser-action-dispatch-scope.ts +67 -0
  84. package/src/browser/index.ts +2 -1
  85. package/src/common/actions/action-dispatcher.ts +292 -25
  86. package/src/common/actions/client-action-handler.ts +2 -2
  87. package/src/common/actions/global-action-provider.ts +2 -2
  88. package/src/common/command/recording-command.spec.ts +1 -2
  89. package/src/common/di/service-identifiers.ts +1 -1
  90. package/src/common/features/clipboard/request-clipboard-data-action-handler.ts +2 -2
  91. package/src/common/features/contextactions/context-actions-provider-registry.spec.ts +2 -2
  92. package/src/common/features/layout/computed-bounds-action-handler.ts +1 -1
  93. package/src/common/features/model/model-submission-handler.ts +1 -1
  94. package/src/common/features/model/request-model-action-handler.ts +1 -1
  95. package/src/common/features/progress/progress-service.ts +1 -1
  96. package/src/common/index.ts +2 -1
  97. package/src/common/protocol/glsp-server.ts +41 -2
  98. package/src/common/test/mock-util.ts +17 -3
  99. package/src/common/utils/action-queue.spec.ts +133 -0
  100. package/src/common/utils/action-queue.ts +107 -0
  101. package/src/common/utils/promise-queue.spec.ts +6 -2
  102. package/src/common/utils/promise-queue.ts +5 -1
  103. package/src/node/actions/action-dispatcher.spec.ts +656 -0
  104. package/src/node/di/app-module.ts +4 -2
  105. package/src/node/di/node-action-dispatch-scope.ts +35 -0
  106. package/src/node/index.ts +2 -1
  107. 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> {
@@ -107,35 +228,37 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
107
228
  responses.push(...response);
108
229
  }
109
230
 
110
- if (this.postUpdateQueue.length > 0 && (UpdateModelAction.is(action) || SetModelAction.is(action))) {
111
- responses.push(...this.postUpdateQueue);
112
- this.postUpdateQueue = [];
113
- }
231
+ // Append post-update actions to responses so they are dispatched in the same inline
232
+ // batch as the handler responses, preserving sequential order.
233
+ responses.push(...this.drainPostUpdateQueue(action));
114
234
 
115
235
  await this.dispatchResponses(responses);
116
236
  }
117
237
 
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));
238
+ protected async executeHandler(handler: ActionHandler, action: Action): Promise<Action[]> {
239
+ const responseActions = await handler.execute(action);
240
+ return responseActions.map(response => respond(action, response));
121
241
  }
122
242
 
123
- protected dispatchResponses(actions: Action[]): Promise<void> {
124
- if (actions.length === 0) {
125
- return Promise.resolve();
243
+ protected async dispatchResponses(actions: Action[]): Promise<void> {
244
+ // Sequential dispatch inside the current dispatch scope. Each response goes inline via
245
+ // the reentrant path, or is intercepted if it resolves a pending request().
246
+ for (const action of actions) {
247
+ await this.dispatch(action);
126
248
  }
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
249
  }
131
250
 
132
- dispatchAll(...actions: MaybeArray<Action>[]): Promise<void> {
251
+ async dispatchAll(...actions: MaybeArray<Action>[]): Promise<void> {
133
252
  if (actions.length === 0) {
134
- return Promise.resolve();
253
+ return;
135
254
  }
136
255
  const flat: Action[] = [];
137
256
  flatPush(flat, actions);
138
- return Promise.all(flat.map(action => this.dispatch(action))).then(() => Promise.resolve());
257
+ // Sequential dispatch: external calls were already FIFO via the queue, but reentrant
258
+ // calls also need deterministic ordering so handlers see each other's effects in order.
259
+ for (const action of flat) {
260
+ await this.dispatch(action);
261
+ }
139
262
  }
140
263
 
141
264
  dispatchAfterNextUpdate(...actions: MaybeArray<Action>[]): void {
@@ -144,8 +267,152 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
144
267
  }
145
268
  }
146
269
 
270
+ request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res> {
271
+ return this.doRequest(action, undefined, true) as Promise<Res>;
272
+ }
273
+
274
+ requestUntil<Res extends ResponseAction>(
275
+ action: RequestAction<Res>,
276
+ timeoutMs: number = action.timeout ?? 2000,
277
+ rejectOnTimeout = false
278
+ ): Promise<Res | undefined> {
279
+ return this.doRequest(action, timeoutMs, rejectOnTimeout);
280
+ }
281
+
282
+ protected doRequest<Res extends ResponseAction>(
283
+ action: RequestAction<Res>,
284
+ timeoutMs: number | undefined,
285
+ rejectOnTimeout: boolean
286
+ ): Promise<Res | undefined> {
287
+ if (!action.requestId || action.requestId === '') {
288
+ action.requestId = this.generateRequestId();
289
+ }
290
+ // Stamp the effective timeout onto the action so the receiving side
291
+ // (handleServerRequest/handleClientRequest) can respect it.
292
+ action.timeout = timeoutMs;
293
+
294
+ const deferred = new Deferred<ResponseAction | undefined>();
295
+ this.pendingRequests.set(action.requestId, deferred);
296
+
297
+ if (timeoutMs !== undefined) {
298
+ const timeout = setTimeout(() => {
299
+ if (this.pendingRequests.delete(action.requestId)) {
300
+ // Keep the requestTimeouts entry briefly as a stale marker so a late response
301
+ // can be filtered, then drop it after a grace period to avoid leaking markers
302
+ // for requests whose late responses never arrive.
303
+ const cleanup = setTimeout(
304
+ () => this.requestTimeouts.delete(action.requestId),
305
+ DefaultActionDispatcher.STALE_TIMEOUT_GRACE_MS
306
+ );
307
+ cleanup.unref?.();
308
+ const message = `Request '${action.requestId}' (${action.kind}) timed out after ${timeoutMs}ms`;
309
+ if (rejectOnTimeout) {
310
+ deferred.reject(new Error(message));
311
+ } else {
312
+ this.logger.info(message);
313
+ deferred.resolve(undefined);
314
+ }
315
+ }
316
+ }, timeoutMs);
317
+
318
+ this.requestTimeouts.set(action.requestId, timeout);
319
+ }
320
+
321
+ // dispatch() routes correctly on its own: external callers queue, handler-internal
322
+ // callers run inline via the ActionDispatchScope. The matching response resolves
323
+ // the deferred out-of-band via interceptPendingResponse().
324
+ const dispatchPromise = this.dispatch(action);
325
+
326
+ dispatchPromise.catch(error => {
327
+ if (this.pendingRequests.delete(action.requestId)) {
328
+ const timeout = this.requestTimeouts.get(action.requestId);
329
+ if (timeout !== undefined) {
330
+ clearTimeout(timeout);
331
+ this.requestTimeouts.delete(action.requestId);
332
+ }
333
+ deferred.reject(error);
334
+ }
335
+ });
336
+
337
+ return deferred.promise as Promise<Res | undefined>;
338
+ }
339
+
340
+ /**
341
+ * If the given action is a {@link SetModelAction} or {@link UpdateModelAction} and there are actions
342
+ * queued via {@link dispatchAfterNextUpdate}, drain and return them.
343
+ *
344
+ * @returns The drained actions, or an empty array if nothing to drain.
345
+ */
346
+ protected drainPostUpdateQueue(action: Action): Action[] {
347
+ if (this.postUpdateQueue.length > 0 && (UpdateModelAction.is(action) || SetModelAction.is(action))) {
348
+ const actions = [...this.postUpdateQueue];
349
+ this.postUpdateQueue = [];
350
+ return actions;
351
+ }
352
+ return [];
353
+ }
354
+
355
+ /**
356
+ * Checks whether the given action is a response matching a pending {@link request} or
357
+ * {@link requestUntil} call. If matched, resolves (or rejects) the corresponding deferred
358
+ * and returns `true` so the caller can short-circuit normal dispatch.
359
+ *
360
+ * For responses with a valid `responseId` but no matching pending request, checks for a stale
361
+ * timeout entry (timed-out request) and clears the `responseId` so the action is not forwarded
362
+ * by {@link ClientActionForwarder}. If no stale entry exists, the `responseId` is left intact
363
+ * for normal forwarding.
364
+ */
365
+ protected interceptPendingResponse(action: Action): boolean {
366
+ if (!ResponseAction.hasValidResponseId(action)) {
367
+ return false;
368
+ }
369
+ const deferred = this.pendingRequests.get(action.responseId);
370
+ if (deferred !== undefined) {
371
+ this.pendingRequests.delete(action.responseId);
372
+ const timeout = this.requestTimeouts.get(action.responseId);
373
+ if (timeout !== undefined) {
374
+ clearTimeout(timeout);
375
+ this.requestTimeouts.delete(action.responseId);
376
+ }
377
+ // Intercepted responses skip doDispatch, so drain post-update actions here when the
378
+ // response is an UpdateModel/SetModel. RejectAction does not trigger a drain; pending
379
+ // post-update actions stay queued until the next successful update.
380
+ const postUpdateActions = this.drainPostUpdateQueue(action);
381
+ if (RejectAction.is(action)) {
382
+ deferred.reject(new Error(`${action.message}${action.detail ? ': ' + action.detail : ''}`));
383
+ } else {
384
+ deferred.resolve(action);
385
+ }
386
+ if (postUpdateActions.length > 0) {
387
+ // Fire-and-forget: callers of request() expect the resolved response, not the
388
+ // unrelated post-update fan-out; awaiting here would couple them unnecessarily.
389
+ this.dispatchResponses(postUpdateActions).catch(error =>
390
+ this.logger.error('Failed to dispatch post-update actions', error)
391
+ );
392
+ }
393
+ return true;
394
+ }
395
+ // Late response for a timed-out request: clear responseId so ClientActionForwarder does
396
+ // not re-emit it to the client.
397
+ const staleTimeout = this.requestTimeouts.get(action.responseId);
398
+ if (staleTimeout !== undefined) {
399
+ clearTimeout(staleTimeout);
400
+ this.requestTimeouts.delete(action.responseId);
401
+ this.logger.debug(`Late response for timed-out request '${action.responseId}', dispatching as normal action`);
402
+ action.responseId = '';
403
+ }
404
+ return false;
405
+ }
406
+
147
407
  dispose(): void {
148
- this.actionQueue.clear();
408
+ // Reject queued actions: no further processing should happen after dispose.
409
+ this.actionQueue.rejectPending(new Error('ActionDispatcher disposed'));
410
+ this.actionQueue.stop();
411
+ this.pendingRequests.forEach((deferred, id) => deferred.reject(new Error(`Request '${id}' cancelled: dispatcher disposed`)));
412
+ this.pendingRequests.clear();
413
+ this.requestTimeouts.forEach(timeout => clearTimeout(timeout));
414
+ this.requestTimeouts.clear();
415
+ this.postUpdateQueue = [];
149
416
  }
150
417
  }
151
418
 
@@ -158,7 +425,7 @@ export class DefaultActionDispatcher implements ActionDispatcher, Disposable {
158
425
  */
159
426
  export function respond(request: Action, response: Action): Action {
160
427
  if (RequestAction.is(request) && ResponseAction.is(response)) {
161
- (response as any).responseId = request.requestId;
428
+ response.responseId = request.requestId;
162
429
  }
163
430
  return response;
164
431
  }
@@ -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
@@ -51,7 +51,7 @@ export class DefaultGlobalActionProvider implements GlobalActionProvider {
51
51
  const container = serverContainer.createChild();
52
52
  const clientSessionModule = createClientSessionModule({
53
53
  clientId: 'tempId',
54
- // eslint-disable-next-line @typescript-eslint/no-empty-function
54
+
55
55
  glspClient: { process: () => {} },
56
56
  clientActionKinds: []
57
57
  });
@@ -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-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
@@ -95,6 +95,7 @@ export * from './session/client-session-factory';
95
95
  export * from './session/client-session-initializer';
96
96
  export * from './session/client-session-listener';
97
97
  export * from './session/client-session-manager';
98
+ export * from './utils/action-queue';
98
99
  export * from './utils/args-util';
99
100
  export * from './utils/client-options-util';
100
101
  export * from './utils/console-logger';
@@ -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
@@ -25,6 +25,9 @@ import {
25
25
  InitializeResult,
26
26
  MaybePromise,
27
27
  MessageAction,
28
+ RejectAction,
29
+ RequestAction,
30
+ ResponseAction,
28
31
  ServerActions,
29
32
  distinctAdd,
30
33
  remove
@@ -68,7 +71,6 @@ export class DefaultGLSPServer implements GLSPServer {
68
71
  protected validateProtocolVersion(params: InitializeParameters): void {
69
72
  if (params.protocolVersion !== DefaultGLSPServer.PROTOCOL_VERSION) {
70
73
  throw new Error(
71
- // eslint-disable-next-line max-len
72
74
  `Protocol version mismatch! The client protocol version ${params.protocolVersion} is not compatible with the server protocol version ${DefaultGLSPServer.PROTOCOL_VERSION}!`
73
75
  );
74
76
  }
@@ -151,9 +153,46 @@ export class DefaultGLSPServer implements GLSPServer {
151
153
  }
152
154
  const action = message.action;
153
155
  ClientAction.mark(action);
156
+ if (RequestAction.is(action)) {
157
+ this.handleClientRequest(clientSession, action, message.clientId);
158
+ return;
159
+ }
154
160
  clientSession.actionDispatcher.dispatch(action).catch(error => this.handleProcessError(message, error));
155
161
  }
156
162
 
163
+ // Fire-and-forget: intentionally not awaited by process()
164
+ protected async handleClientRequest(
165
+ clientSession: ClientSession,
166
+ action: RequestAction<ResponseAction>,
167
+ clientId: string
168
+ ): Promise<void> {
169
+ try {
170
+ const response =
171
+ action.timeout !== undefined
172
+ ? await clientSession.actionDispatcher.requestUntil(action, action.timeout, true)
173
+ : await clientSession.actionDispatcher.request(action);
174
+ if (response) {
175
+ this.sendResponseToClient(clientId, response);
176
+ }
177
+ } catch (error) {
178
+ const detail = error instanceof GLSPServerError ? error.cause?.toString?.() : error?.toString?.();
179
+ this.logger.error(`Failed to handle request '${action.kind}' (${action.requestId}):`, detail);
180
+ try {
181
+ const reject = RejectAction.create(`Failed to handle request '${action.kind}' (${action.requestId})`, {
182
+ responseId: action.requestId,
183
+ detail
184
+ });
185
+ this.sendResponseToClient(clientId, reject);
186
+ } catch (sendError) {
187
+ this.logger.error(`Failed to send rejection for request '${action.requestId}':`, sendError);
188
+ }
189
+ }
190
+ }
191
+
192
+ protected sendResponseToClient(clientId: string, response: ResponseAction): void {
193
+ this.sendToClient({ clientId, action: response });
194
+ }
195
+
157
196
  protected handleProcessError(message: ActionMessage, reason: any): void | PromiseLike<void> {
158
197
  let errorMsg = `Could not process action: '${message.action.kind}`;
159
198
  this.logger.error(errorMsg);
@@ -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
@@ -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
- /* eslint-disable @typescript-eslint/no-empty-function */
16
+
17
17
  // Stub-implementation classes used for unit testing
18
18
 
19
19
  import { GEdge, GModelElement, GModelElementConstructor, GNode } from '@eclipse-glsp/graph';
@@ -30,7 +30,9 @@ import {
30
30
  MaybeArray,
31
31
  MaybePromise,
32
32
  Point,
33
+ RequestAction,
33
34
  RequestEditValidationAction,
35
+ ResponseAction,
34
36
  ShapeTypeHint,
35
37
  ValidationStatus
36
38
  } from '@eclipse-glsp/protocol';
@@ -95,7 +97,7 @@ export function createClientSession(
95
97
  export class StubActionHandler implements ActionHandler {
96
98
  constructor(public actionKinds: string[]) {}
97
99
 
98
- execute(action: Action): Action[] {
100
+ execute(action: Action): MaybePromise<Action[]> {
99
101
  return [];
100
102
  }
101
103
  }
@@ -136,6 +138,18 @@ export class StubActionDispatcher implements ActionDispatcher {
136
138
  dispatchAll(...actions: MaybeArray<Action>[]): Promise<void> {
137
139
  return Promise.resolve();
138
140
  }
141
+
142
+ request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res> {
143
+ return Promise.reject(new Error('Not implemented in stub'));
144
+ }
145
+
146
+ requestUntil<Res extends ResponseAction>(
147
+ action: RequestAction<Res>,
148
+ timeoutMs?: number,
149
+ rejectOnTimeout?: boolean
150
+ ): Promise<Res | undefined> {
151
+ return Promise.reject(new Error('Not implemented in stub'));
152
+ }
139
153
  }
140
154
 
141
155
  export class StubClientSessionFactory implements ClientSessionFactory {