@eclipse-glsp/server 2.6.0 → 2.7.0-next.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/browser/di/app-module.d.ts +1 -1
- package/lib/browser/di/app-module.d.ts.map +1 -1
- package/lib/browser/di/app-module.js +5 -1
- package/lib/browser/di/app-module.js.map +1 -1
- package/lib/browser/di/browser-action-dispatch-scope.d.ts +39 -0
- package/lib/browser/di/browser-action-dispatch-scope.d.ts.map +1 -0
- package/lib/browser/di/browser-action-dispatch-scope.js +77 -0
- package/lib/browser/di/browser-action-dispatch-scope.js.map +1 -0
- package/lib/browser/index.d.ts +2 -1
- package/lib/browser/index.d.ts.map +1 -1
- package/lib/browser/index.js +2 -1
- package/lib/browser/index.js.map +1 -1
- package/lib/common/actions/action-dispatcher.d.ts +107 -5
- package/lib/common/actions/action-dispatcher.d.ts.map +1 -1
- package/lib/common/actions/action-dispatcher.js +199 -26
- package/lib/common/actions/action-dispatcher.js.map +1 -1
- package/lib/common/actions/client-action-handler.d.ts +1 -1
- package/lib/common/actions/client-action-handler.js +2 -2
- package/lib/common/actions/client-action-handler.js.map +1 -1
- package/lib/common/actions/global-action-provider.js +1 -2
- package/lib/common/actions/global-action-provider.js.map +1 -1
- package/lib/common/di/service-identifiers.d.ts +1 -1
- package/lib/common/features/clipboard/request-clipboard-data-action-handler.d.ts +1 -1
- package/lib/common/features/clipboard/request-clipboard-data-action-handler.js +1 -2
- package/lib/common/features/clipboard/request-clipboard-data-action-handler.js.map +1 -1
- package/lib/common/features/layout/computed-bounds-action-handler.d.ts +1 -1
- package/lib/common/features/model/model-submission-handler.d.ts +1 -1
- package/lib/common/features/model/model-submission-handler.js +1 -1
- package/lib/common/features/model/request-model-action-handler.d.ts +1 -1
- package/lib/common/features/model/request-model-action-handler.js +1 -1
- package/lib/common/features/progress/progress-service.js +1 -1
- package/lib/common/index.d.ts +2 -1
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +2 -1
- package/lib/common/index.js.map +1 -1
- package/lib/common/protocol/glsp-server.d.ts +4 -2
- package/lib/common/protocol/glsp-server.d.ts.map +1 -1
- package/lib/common/protocol/glsp-server.js +35 -4
- package/lib/common/protocol/glsp-server.js.map +1 -1
- package/lib/common/utils/action-queue.d.ts +58 -0
- package/lib/common/utils/action-queue.d.ts.map +1 -0
- package/lib/common/utils/action-queue.js +99 -0
- package/lib/common/utils/action-queue.js.map +1 -0
- package/lib/common/utils/promise-queue.d.ts +5 -1
- package/lib/common/utils/promise-queue.d.ts.map +1 -1
- package/lib/common/utils/promise-queue.js +5 -1
- package/lib/common/utils/promise-queue.js.map +1 -1
- package/lib/node/di/app-module.d.ts +1 -1
- package/lib/node/di/app-module.d.ts.map +1 -1
- package/lib/node/di/app-module.js +2 -0
- package/lib/node/di/app-module.js.map +1 -1
- package/lib/node/di/node-action-dispatch-scope.d.ts +26 -0
- package/lib/node/di/node-action-dispatch-scope.d.ts.map +1 -0
- package/lib/node/di/node-action-dispatch-scope.js +45 -0
- package/lib/node/di/node-action-dispatch-scope.js.map +1 -0
- package/lib/node/index.d.ts +2 -1
- package/lib/node/index.d.ts.map +1 -1
- package/lib/node/index.js +2 -1
- package/lib/node/index.js.map +1 -1
- package/package.json +5 -5
- package/src/browser/di/app-module.ts +6 -2
- package/src/browser/di/browser-action-dispatch-scope.spec.ts +75 -0
- package/src/browser/di/browser-action-dispatch-scope.ts +67 -0
- package/src/browser/index.ts +2 -1
- package/src/common/actions/action-dispatcher.ts +292 -25
- package/src/common/actions/client-action-handler.ts +2 -2
- package/src/common/actions/global-action-provider.ts +2 -2
- package/src/common/command/recording-command.spec.ts +1 -2
- package/src/common/di/service-identifiers.ts +1 -1
- package/src/common/features/clipboard/request-clipboard-data-action-handler.ts +2 -2
- package/src/common/features/contextactions/context-actions-provider-registry.spec.ts +2 -2
- package/src/common/features/layout/computed-bounds-action-handler.ts +1 -1
- package/src/common/features/model/model-submission-handler.ts +1 -1
- package/src/common/features/model/request-model-action-handler.ts +1 -1
- package/src/common/features/progress/progress-service.ts +1 -1
- package/src/common/index.ts +2 -1
- package/src/common/protocol/glsp-server.ts +41 -2
- package/src/common/test/mock-util.ts +17 -3
- package/src/common/utils/action-queue.spec.ts +133 -0
- package/src/common/utils/action-queue.ts +107 -0
- package/src/common/utils/promise-queue.spec.ts +6 -2
- package/src/common/utils/promise-queue.ts +5 -1
- package/src/node/actions/action-dispatcher.spec.ts +656 -0
- package/src/node/di/app-module.ts +4 -2
- package/src/node/di/node-action-dispatch-scope.ts +35 -0
- package/src/node/index.ts +2 -1
- package/src/common/actions/action-dispatcher.spec.ts +0 -291
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (c) 2026 EclipseSource and others.
|
|
3
|
+
*
|
|
4
|
+
* This program and the accompanying materials are made available under the
|
|
5
|
+
* terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
* http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
*
|
|
8
|
+
* This Source Code may also be made available under the following Secondary
|
|
9
|
+
* Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
* with the GNU Classpath Exception which is available at
|
|
12
|
+
* https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
*
|
|
14
|
+
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
|
+
********************************************************************************/
|
|
16
|
+
|
|
17
|
+
import { Action } from '@eclipse-glsp/protocol';
|
|
18
|
+
import { injectable } from 'inversify';
|
|
19
|
+
import { ActionDispatchScope } from '../../common/actions/action-dispatcher';
|
|
20
|
+
import { ClientAction } from '../../common/protocol/client-action';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Browser-compatible {@link ActionDispatchScope} backed by a single boolean flag, used because
|
|
24
|
+
* available `AsyncLocalStorage` polyfills do not work reliably across browser engines (e.g. V8).
|
|
25
|
+
*
|
|
26
|
+
* The flag cannot distinguish "still inside the handler's async continuation" from "unrelated
|
|
27
|
+
* event fired during the handler's await". Any dispatch arriving in such a gap is observed as
|
|
28
|
+
* reentrant and routed inline. Client-originated actions are explicitly treated as non-reentrant
|
|
29
|
+
* to cover the dominant case, but server-side dispatches from non-handler contexts (timer
|
|
30
|
+
* callbacks, event listeners, adopter code) cannot be filtered this way and may interleave with
|
|
31
|
+
* the in-flight handler.
|
|
32
|
+
*
|
|
33
|
+
* The dispatcher normally serializes handler execution; the inline interleaving breaks that
|
|
34
|
+
* guarantee. A handler that pauses on `await` may resume to find that another handler has mutated
|
|
35
|
+
* state in between (model state, command stack, caches), leading to unexpected behavior.
|
|
36
|
+
* Avoid dispatching from non-handler contexts where possible.
|
|
37
|
+
*/
|
|
38
|
+
@injectable()
|
|
39
|
+
export class BrowserActionDispatchScope implements ActionDispatchScope {
|
|
40
|
+
protected active = false;
|
|
41
|
+
|
|
42
|
+
// Assumes serial invocation by the dispatcher's queue processor; concurrent enter() calls
|
|
43
|
+
// would corrupt the prior-restore logic and leave the flag stuck.
|
|
44
|
+
enter<R>(callback: () => R): R {
|
|
45
|
+
const prior = this.active;
|
|
46
|
+
this.active = true;
|
|
47
|
+
let result: R;
|
|
48
|
+
try {
|
|
49
|
+
result = callback();
|
|
50
|
+
} catch (error) {
|
|
51
|
+
this.active = prior;
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
if (result instanceof Promise) {
|
|
55
|
+
// Cast required because TS cannot prove the .finally() result matches the generic R.
|
|
56
|
+
return result.finally(() => {
|
|
57
|
+
this.active = prior;
|
|
58
|
+
}) as unknown as R;
|
|
59
|
+
}
|
|
60
|
+
this.active = prior;
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
isReentrant(action: Action): boolean {
|
|
65
|
+
return this.active && !ClientAction.is(action);
|
|
66
|
+
}
|
|
67
|
+
}
|
package/src/browser/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/********************************************************************************
|
|
2
|
-
* Copyright (c) 2022-
|
|
2
|
+
* Copyright (c) 2022-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
|
|
@@ -14,5 +14,6 @@
|
|
|
14
14
|
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
15
|
********************************************************************************/
|
|
16
16
|
export * from './di/app-module';
|
|
17
|
+
export * from './di/browser-action-dispatch-scope';
|
|
17
18
|
export * from './launch/worker-server-launcher';
|
|
18
19
|
export * from './reexport';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/********************************************************************************
|
|
2
|
-
* Copyright (c) 2022-
|
|
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
|
-
|
|
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
|
-
//
|
|
89
|
-
if (this.
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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,
|
|
119
|
-
const responseActions = await handler.execute(
|
|
120
|
-
return responseActions.map(
|
|
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
|
-
|
|
125
|
-
|
|
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
|
|
253
|
+
return;
|
|
135
254
|
}
|
|
136
255
|
const flat: Action[] = [];
|
|
137
256
|
flatPush(flat, actions);
|
|
138
|
-
|
|
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
|
-
|
|
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
|
-
|
|
428
|
+
response.responseId = request.requestId;
|
|
162
429
|
}
|
|
163
430
|
return response;
|
|
164
431
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/********************************************************************************
|
|
2
|
-
* Copyright (c) 2022-
|
|
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.
|
|
57
|
+
return this.actionKinds.has(action.kind) || ResponseAction.hasValidResponseId(action);
|
|
58
58
|
}
|
|
59
59
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/********************************************************************************
|
|
2
|
-
* Copyright (c) 2022-
|
|
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
|
-
|
|
54
|
+
|
|
55
55
|
glspClient: { process: () => {} },
|
|
56
56
|
clientActionKinds: []
|
|
57
57
|
});
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/********************************************************************************
|
|
2
|
-
* Copyright (c) 2023-
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
package/src/common/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/********************************************************************************
|
|
2
|
-
* Copyright (c) 2022-
|
|
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';
|