@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.
- 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 +205 -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.d.ts.map +1 -1
- package/lib/common/actions/global-action-provider.js +2 -3
- package/lib/common/actions/global-action-provider.js.map +1 -1
- package/lib/common/di/client-session-module.d.ts +8 -1
- package/lib/common/di/client-session-module.d.ts.map +1 -1
- package/lib/common/di/client-session-module.js +9 -1
- package/lib/common/di/client-session-module.js.map +1 -1
- package/lib/common/di/server-module.d.ts +3 -2
- package/lib/common/di/server-module.d.ts.map +1 -1
- package/lib/common/di/server-module.js +5 -1
- package/lib/common/di/server-module.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 +3 -1
- package/lib/common/index.d.ts.map +1 -1
- package/lib/common/index.js +3 -1
- package/lib/common/index.js.map +1 -1
- package/lib/common/launch/jsonrpc-server-launcher.d.ts +1 -1
- package/lib/common/launch/jsonrpc-server-launcher.d.ts.map +1 -1
- package/lib/common/launch/jsonrpc-server-launcher.js +4 -1
- package/lib/common/launch/jsonrpc-server-launcher.js.map +1 -1
- package/lib/common/operations/create-operation-handler.d.ts +8 -2
- package/lib/common/operations/create-operation-handler.d.ts.map +1 -1
- package/lib/common/operations/create-operation-handler.js +16 -2
- package/lib/common/operations/create-operation-handler.js.map +1 -1
- package/lib/common/protocol/glsp-server.d.ts +10 -3
- package/lib/common/protocol/glsp-server.d.ts.map +1 -1
- package/lib/common/protocol/glsp-server.js +58 -6
- package/lib/common/protocol/glsp-server.js.map +1 -1
- package/lib/common/session/client-session-manager.d.ts +8 -1
- package/lib/common/session/client-session-manager.d.ts.map +1 -1
- package/lib/common/session/client-session-manager.js +4 -1
- package/lib/common/session/client-session-manager.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 +298 -25
- package/src/common/actions/client-action-handler.ts +2 -2
- package/src/common/actions/global-action-provider.ts +3 -4
- package/src/common/command/recording-command.spec.ts +1 -2
- package/src/common/di/client-session-module.ts +9 -1
- package/src/common/di/server-module.ts +9 -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 +3 -1
- package/src/common/launch/jsonrpc-server-launcher.ts +4 -1
- package/src/common/operations/create-operation-handler.ts +16 -3
- package/src/common/protocol/glsp-server.ts +64 -3
- package/src/common/session/client-session-manager.ts +12 -1
- package/src/common/test/mock-util.ts +21 -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 +678 -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
|
@@ -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
|
|
@@ -38,6 +38,12 @@ export interface CreateEdgeOperationHandler extends OperationHandler {
|
|
|
38
38
|
createCommand(operation: CreateEdgeOperation): MaybePromise<Command | undefined>;
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
export namespace CreateEdgeOperationHandler {
|
|
42
|
+
export function is(object: unknown): object is CreateEdgeOperationHandler {
|
|
43
|
+
return CreateOperationHandler.is(object) && object.operationType === CreateEdgeOperation.KIND;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
41
47
|
export interface CreateNodeOperationHandler extends OperationHandler {
|
|
42
48
|
readonly label: string;
|
|
43
49
|
elementTypeIds: string[];
|
|
@@ -45,14 +51,19 @@ export interface CreateNodeOperationHandler extends OperationHandler {
|
|
|
45
51
|
getTriggerActions(): TriggerNodeCreationAction[];
|
|
46
52
|
createCommand(operation: CreateNodeOperation): MaybePromise<Command | undefined>;
|
|
47
53
|
}
|
|
54
|
+
|
|
55
|
+
export namespace CreateNodeOperationHandler {
|
|
56
|
+
export function is(object: unknown): object is CreateNodeOperationHandler {
|
|
57
|
+
return CreateOperationHandler.is(object) && object.operationType === CreateNodeOperation.KIND;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
48
61
|
/**
|
|
49
62
|
* A special {@link OperationHandler} that is responsible for the handling of a node or edge creation operation. Depending on its
|
|
50
63
|
* operation type the triggered actions are {@link TriggerNodeCreationAction} or {@link TriggerEdgeCreationAction}s.
|
|
51
64
|
*/
|
|
52
65
|
export type CreateOperationHandler = CreateNodeOperationHandler | CreateEdgeOperationHandler;
|
|
53
66
|
|
|
54
|
-
export type CreateOperationKind = typeof CreateNodeOperation.KIND | typeof CreateEdgeOperation.KIND;
|
|
55
|
-
|
|
56
67
|
export namespace CreateOperationHandler {
|
|
57
68
|
export function is(object: unknown): object is CreateOperationHandler {
|
|
58
69
|
return (
|
|
@@ -64,3 +75,5 @@ export namespace CreateOperationHandler {
|
|
|
64
75
|
);
|
|
65
76
|
}
|
|
66
77
|
}
|
|
78
|
+
|
|
79
|
+
export type CreateOperationKind = typeof CreateNodeOperation.KIND | typeof CreateEdgeOperation.KIND;
|
|
@@ -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
|
|
@@ -19,12 +19,16 @@ import {
|
|
|
19
19
|
DisposeClientSessionParameters,
|
|
20
20
|
GLSPClientProxy,
|
|
21
21
|
GLSPServer,
|
|
22
|
+
GLSPServerInitializer,
|
|
22
23
|
GLSPServerListener,
|
|
23
24
|
InitializeClientSessionParameters,
|
|
24
25
|
InitializeParameters,
|
|
25
26
|
InitializeResult,
|
|
26
27
|
MaybePromise,
|
|
27
28
|
MessageAction,
|
|
29
|
+
RejectAction,
|
|
30
|
+
RequestAction,
|
|
31
|
+
ResponseAction,
|
|
28
32
|
ServerActions,
|
|
29
33
|
distinctAdd,
|
|
30
34
|
remove
|
|
@@ -60,7 +64,10 @@ export class DefaultGLSPServer implements GLSPServer {
|
|
|
60
64
|
protected clientSessions: Map<string, ClientSession>;
|
|
61
65
|
protected serverListeners: GLSPServerListener[] = [];
|
|
62
66
|
|
|
63
|
-
constructor(
|
|
67
|
+
constructor(
|
|
68
|
+
@multiInject(GLSPServerListener) @optional() serverListeners: GLSPServerListener[] = [],
|
|
69
|
+
@multiInject(GLSPServerInitializer) @optional() protected initializers: GLSPServerInitializer[] = []
|
|
70
|
+
) {
|
|
64
71
|
this.clientSessions = new Map<string, ClientSession>();
|
|
65
72
|
serverListeners.forEach(listener => this.addListener(listener));
|
|
66
73
|
}
|
|
@@ -68,7 +75,6 @@ export class DefaultGLSPServer implements GLSPServer {
|
|
|
68
75
|
protected validateProtocolVersion(params: InitializeParameters): void {
|
|
69
76
|
if (params.protocolVersion !== DefaultGLSPServer.PROTOCOL_VERSION) {
|
|
70
77
|
throw new Error(
|
|
71
|
-
// eslint-disable-next-line max-len
|
|
72
78
|
`Protocol version mismatch! The client protocol version ${params.protocolVersion} is not compatible with the server protocol version ${DefaultGLSPServer.PROTOCOL_VERSION}!`
|
|
73
79
|
);
|
|
74
80
|
}
|
|
@@ -101,12 +107,30 @@ export class DefaultGLSPServer implements GLSPServer {
|
|
|
101
107
|
|
|
102
108
|
let result = { protocolVersion: DefaultGLSPServer.PROTOCOL_VERSION, serverActions };
|
|
103
109
|
|
|
110
|
+
result = await this.initializeServer(params, result);
|
|
111
|
+
// keep for backwards compatibility
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated
|
|
104
113
|
result = await this.handleInitializeArgs(result, params.args);
|
|
105
114
|
this.getListenersToNotify('serverInitialized').forEach((listener: GLSPServerListener) => listener.serverInitialized!(this));
|
|
106
115
|
this.initializeResult = result;
|
|
107
116
|
return result;
|
|
108
117
|
}
|
|
109
118
|
|
|
119
|
+
protected async initializeServer(params: InitializeParameters, result: InitializeResult): Promise<InitializeResult> {
|
|
120
|
+
for (const initializer of this.initializers) {
|
|
121
|
+
try {
|
|
122
|
+
result = await initializer.initializeServer(this, params, result);
|
|
123
|
+
} catch (error: unknown) {
|
|
124
|
+
this.logger.error(`Error during server initialization from ${initializer.constructor.name}:`, error);
|
|
125
|
+
throw error;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return result;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* @deprecated Register a `GLSPServerInitializer` instead.
|
|
133
|
+
*/
|
|
110
134
|
protected handleInitializeArgs(result: InitializeResult, args: Args | undefined): MaybePromise<InitializeResult> {
|
|
111
135
|
return result;
|
|
112
136
|
}
|
|
@@ -151,9 +175,46 @@ export class DefaultGLSPServer implements GLSPServer {
|
|
|
151
175
|
}
|
|
152
176
|
const action = message.action;
|
|
153
177
|
ClientAction.mark(action);
|
|
178
|
+
if (RequestAction.is(action)) {
|
|
179
|
+
this.handleClientRequest(clientSession, action, message.clientId);
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
154
182
|
clientSession.actionDispatcher.dispatch(action).catch(error => this.handleProcessError(message, error));
|
|
155
183
|
}
|
|
156
184
|
|
|
185
|
+
// Fire-and-forget: intentionally not awaited by process()
|
|
186
|
+
protected async handleClientRequest(
|
|
187
|
+
clientSession: ClientSession,
|
|
188
|
+
action: RequestAction<ResponseAction>,
|
|
189
|
+
clientId: string
|
|
190
|
+
): Promise<void> {
|
|
191
|
+
try {
|
|
192
|
+
const response =
|
|
193
|
+
action.timeout !== undefined
|
|
194
|
+
? await clientSession.actionDispatcher.requestUntil(action, action.timeout, true)
|
|
195
|
+
: await clientSession.actionDispatcher.request(action);
|
|
196
|
+
if (response) {
|
|
197
|
+
this.sendResponseToClient(clientId, response);
|
|
198
|
+
}
|
|
199
|
+
} catch (error) {
|
|
200
|
+
const detail = error instanceof GLSPServerError ? error.cause?.toString?.() : error?.toString?.();
|
|
201
|
+
this.logger.error(`Failed to handle request '${action.kind}' (${action.requestId}):`, detail);
|
|
202
|
+
try {
|
|
203
|
+
const reject = RejectAction.create(`Failed to handle request '${action.kind}' (${action.requestId})`, {
|
|
204
|
+
responseId: action.requestId,
|
|
205
|
+
detail
|
|
206
|
+
});
|
|
207
|
+
this.sendResponseToClient(clientId, reject);
|
|
208
|
+
} catch (sendError) {
|
|
209
|
+
this.logger.error(`Failed to send rejection for request '${action.requestId}':`, sendError);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
protected sendResponseToClient(clientId: string, response: ResponseAction): void {
|
|
215
|
+
this.sendToClient({ clientId, action: response });
|
|
216
|
+
}
|
|
217
|
+
|
|
157
218
|
protected handleProcessError(message: ActionMessage, reason: any): void | PromiseLike<void> {
|
|
158
219
|
let errorMsg = `Could not process action: '${message.action.kind}`;
|
|
159
220
|
this.logger.error(errorMsg);
|
|
@@ -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
|
|
@@ -47,6 +47,13 @@ export interface ClientSessionManager {
|
|
|
47
47
|
*/
|
|
48
48
|
getSession(clientSessionId: string): ClientSession | undefined;
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Return all currently active {@link ClientSession}s.
|
|
52
|
+
*
|
|
53
|
+
* @returns An array of all currently active {@link ClientSession}s.
|
|
54
|
+
*/
|
|
55
|
+
getSessions(): Array<ClientSession>;
|
|
56
|
+
|
|
50
57
|
/**
|
|
51
58
|
* Return all currently active {@link ClientSession}s for the given diagram type.
|
|
52
59
|
*
|
|
@@ -129,6 +136,10 @@ export class DefaultClientSessionManager implements ClientSessionManager, GLSPSe
|
|
|
129
136
|
return this.clientSessions.get(clientSessionId);
|
|
130
137
|
}
|
|
131
138
|
|
|
139
|
+
getSessions(): ClientSession[] {
|
|
140
|
+
return Array.from(this.clientSessions.values());
|
|
141
|
+
}
|
|
142
|
+
|
|
132
143
|
getSessionsByType(diagramType: string): ClientSession[] {
|
|
133
144
|
return Array.from(this.clientSessions.values()).filter(session => session.diagramType === diagramType);
|
|
134
145
|
}
|
|
@@ -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
|
|
@@ -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
|
-
|
|
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 {
|
|
@@ -155,6 +169,10 @@ export class StubClientSessionManager implements ClientSessionManager {
|
|
|
155
169
|
return undefined;
|
|
156
170
|
}
|
|
157
171
|
|
|
172
|
+
getSessions(): ClientSession[] {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
|
|
158
176
|
getSessionsByType(diagramType: string): ClientSession[] {
|
|
159
177
|
return [];
|
|
160
178
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
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
|
+
import { expect } from 'chai';
|
|
17
|
+
import { expectToThrowAsync } from '../test/mock-util';
|
|
18
|
+
import { ActionQueue } from './action-queue';
|
|
19
|
+
|
|
20
|
+
describe('ActionQueue', () => {
|
|
21
|
+
it('yields pushed items in FIFO order', async () => {
|
|
22
|
+
const channel = new ActionQueue<number>();
|
|
23
|
+
const consumed: number[] = [];
|
|
24
|
+
|
|
25
|
+
const consumer = (async (): Promise<void> => {
|
|
26
|
+
for await (const entry of channel.consume()) {
|
|
27
|
+
consumed.push(entry.item);
|
|
28
|
+
entry.resolve();
|
|
29
|
+
}
|
|
30
|
+
})();
|
|
31
|
+
|
|
32
|
+
await Promise.all([channel.push(1), channel.push(2), channel.push(3)]);
|
|
33
|
+
channel.stop();
|
|
34
|
+
await consumer;
|
|
35
|
+
|
|
36
|
+
expect(consumed).to.deep.equal([1, 2, 3]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it('resolves the push promise once the consumer resolves the entry', async () => {
|
|
40
|
+
const channel = new ActionQueue<string>();
|
|
41
|
+
let entryResolver: (() => void) | undefined;
|
|
42
|
+
|
|
43
|
+
const consumer = (async (): Promise<void> => {
|
|
44
|
+
for await (const entry of channel.consume()) {
|
|
45
|
+
entryResolver = entry.resolve;
|
|
46
|
+
return;
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
|
|
50
|
+
const pushed = channel.push('a');
|
|
51
|
+
// Give the consumer a turn to pick up the entry.
|
|
52
|
+
await Promise.resolve();
|
|
53
|
+
await consumer;
|
|
54
|
+
expect(entryResolver).to.exist;
|
|
55
|
+
entryResolver!();
|
|
56
|
+
await pushed;
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('propagates reject() from the consumer back to the pushing caller', async () => {
|
|
60
|
+
const channel = new ActionQueue<number>();
|
|
61
|
+
|
|
62
|
+
const consumer = (async (): Promise<void> => {
|
|
63
|
+
for await (const entry of channel.consume()) {
|
|
64
|
+
entry.reject(new Error('boom'));
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
})();
|
|
68
|
+
|
|
69
|
+
const pushed = channel.push(1);
|
|
70
|
+
await consumer;
|
|
71
|
+
await expectToThrowAsync(() => pushed, 'boom');
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('rejects push() after stop()', async () => {
|
|
75
|
+
const channel = new ActionQueue<number>();
|
|
76
|
+
channel.stop();
|
|
77
|
+
await expectToThrowAsync(() => channel.push(1), 'ActionQueue is stopped');
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('consumer exits after stop() and drain', async () => {
|
|
81
|
+
const channel = new ActionQueue<number>();
|
|
82
|
+
const consumed: number[] = [];
|
|
83
|
+
|
|
84
|
+
const consumer = (async (): Promise<void> => {
|
|
85
|
+
for await (const entry of channel.consume()) {
|
|
86
|
+
consumed.push(entry.item);
|
|
87
|
+
entry.resolve();
|
|
88
|
+
}
|
|
89
|
+
})();
|
|
90
|
+
|
|
91
|
+
await channel.push(1);
|
|
92
|
+
await channel.push(2);
|
|
93
|
+
channel.stop();
|
|
94
|
+
await consumer;
|
|
95
|
+
|
|
96
|
+
expect(consumed).to.deep.equal([1, 2]);
|
|
97
|
+
expect(channel.isStopped).to.be.true;
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('rejectPending() rejects all queued push() promises without stopping', async () => {
|
|
101
|
+
const channel = new ActionQueue<number>();
|
|
102
|
+
const pushes = [channel.push(1), channel.push(2)];
|
|
103
|
+
expect(channel.size).to.equal(2);
|
|
104
|
+
|
|
105
|
+
channel.rejectPending(new Error('cleared'));
|
|
106
|
+
|
|
107
|
+
await expectToThrowAsync(() => pushes[0], 'cleared');
|
|
108
|
+
await expectToThrowAsync(() => pushes[1], 'cleared');
|
|
109
|
+
expect(channel.size).to.equal(0);
|
|
110
|
+
expect(channel.isStopped).to.be.false;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('size reflects the number of unconsumed entries', async () => {
|
|
114
|
+
const channel = new ActionQueue<number>();
|
|
115
|
+
channel.push(1);
|
|
116
|
+
channel.push(2);
|
|
117
|
+
channel.push(3);
|
|
118
|
+
expect(channel.size).to.equal(3);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('throws when a second consumer is started', async () => {
|
|
122
|
+
const channel = new ActionQueue<number>();
|
|
123
|
+
const first = channel.consume();
|
|
124
|
+
// Kick off the first consumer so it registers as the active consumer.
|
|
125
|
+
const firstStep = first.next();
|
|
126
|
+
|
|
127
|
+
const second = channel.consume();
|
|
128
|
+
await expectToThrowAsync(() => second.next().then(() => undefined), 'ActionQueue supports only a single consumer');
|
|
129
|
+
|
|
130
|
+
channel.stop();
|
|
131
|
+
await firstStep;
|
|
132
|
+
});
|
|
133
|
+
});
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
/**
|
|
18
|
+
* An entry yielded by {@link ActionQueue.consume}. The consumer must call either
|
|
19
|
+
* `resolve()` or `reject(error)` exactly once after processing `item`.
|
|
20
|
+
*/
|
|
21
|
+
export interface ActionQueueEntry<T> {
|
|
22
|
+
item: T;
|
|
23
|
+
resolve: () => void;
|
|
24
|
+
reject: (error: unknown) => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Producer/consumer queue with a single async consumer loop. Items are processed in FIFO order.
|
|
29
|
+
*/
|
|
30
|
+
export class ActionQueue<T> {
|
|
31
|
+
protected queue: ActionQueueEntry<T>[] = [];
|
|
32
|
+
protected notify: (() => void) | undefined;
|
|
33
|
+
protected stopped = false;
|
|
34
|
+
protected consuming = false;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Enqueues an item. The returned promise settles when the consumer finishes processing it,
|
|
38
|
+
* propagating results back to the producer. Rejects immediately if the queue has been stopped.
|
|
39
|
+
*/
|
|
40
|
+
push(item: T): Promise<void> {
|
|
41
|
+
if (this.stopped) {
|
|
42
|
+
return Promise.reject(new Error('ActionQueue is stopped'));
|
|
43
|
+
}
|
|
44
|
+
return new Promise((resolve, reject) => {
|
|
45
|
+
this.queue.push({ item, resolve, reject });
|
|
46
|
+
this.notify?.();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Yields pending entries, suspending until the next {@link push} when the queue is empty. Exits
|
|
52
|
+
* once the queue is stopped and has been drained.
|
|
53
|
+
*
|
|
54
|
+
* Single-consumer: calling `consume()` a second time throws an error.
|
|
55
|
+
*/
|
|
56
|
+
async *consume(): AsyncGenerator<ActionQueueEntry<T>> {
|
|
57
|
+
if (this.consuming) {
|
|
58
|
+
throw new Error('ActionQueue supports only a single consumer');
|
|
59
|
+
}
|
|
60
|
+
this.consuming = true;
|
|
61
|
+
try {
|
|
62
|
+
while (!this.stopped || this.queue.length > 0) {
|
|
63
|
+
while (this.queue.length > 0) {
|
|
64
|
+
yield this.queue.shift()!;
|
|
65
|
+
}
|
|
66
|
+
if (this.stopped) {
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
await new Promise<void>(resolve => {
|
|
70
|
+
this.notify = resolve;
|
|
71
|
+
});
|
|
72
|
+
this.notify = undefined;
|
|
73
|
+
}
|
|
74
|
+
} finally {
|
|
75
|
+
this.consuming = false;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Stops the queue. Further {@link push} calls reject. The consumer loop exits after
|
|
81
|
+
* the remaining queued entries have been yielded (or immediately if the queue is empty).
|
|
82
|
+
*/
|
|
83
|
+
stop(): void {
|
|
84
|
+
this.stopped = true;
|
|
85
|
+
this.notify?.();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Rejects all queued entries with the given reason so producers awaiting their
|
|
90
|
+
* `push()` promises do not hang. Does not stop the queue.
|
|
91
|
+
*/
|
|
92
|
+
rejectPending(reason: Error = new Error('ActionQueue cleared')): void {
|
|
93
|
+
const pending = this.queue;
|
|
94
|
+
this.queue = [];
|
|
95
|
+
for (const entry of pending) {
|
|
96
|
+
entry.reject(reason);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
get size(): number {
|
|
101
|
+
return this.queue.length;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get isStopped(): boolean {
|
|
105
|
+
return this.stopped;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -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
|
|
@@ -14,8 +14,10 @@
|
|
|
14
14
|
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
|
|
15
15
|
********************************************************************************/
|
|
16
16
|
import { delay } from '../test/mock-util';
|
|
17
|
-
|
|
17
|
+
|
|
18
18
|
import { expect } from 'chai';
|
|
19
|
+
// eslint-disable-next-line import-x/no-deprecated
|
|
20
|
+
import { PromiseQueue } from './promise-queue';
|
|
19
21
|
|
|
20
22
|
// Helper types and functions that are needed for test setup
|
|
21
23
|
|
|
@@ -74,11 +76,13 @@ function newTestPromise(resolveTime: number): TestPromise {
|
|
|
74
76
|
return { state, promise };
|
|
75
77
|
}
|
|
76
78
|
|
|
79
|
+
// eslint-disable-next-line @typescript-eslint/no-deprecated, import-x/no-deprecated
|
|
77
80
|
let queue = new PromiseQueue();
|
|
78
81
|
|
|
79
82
|
// Test execution
|
|
80
83
|
describe('test PromiseQueue', () => {
|
|
81
84
|
beforeEach(() => {
|
|
85
|
+
// eslint-disable-next-line import-x/no-deprecated, @typescript-eslint/no-deprecated
|
|
82
86
|
queue = new PromiseQueue();
|
|
83
87
|
});
|
|
84
88
|
it('enqueue - one element', async () => {
|
|
@@ -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
|
|
@@ -29,6 +29,10 @@ export interface PromiseQueueElement<T = void> {
|
|
|
29
29
|
* of promises. Promises that are put in this queue are processed one by one.
|
|
30
30
|
* i.e. After the first promise in the queue is resolved, it will be removed from the queue and the resolving of the
|
|
31
31
|
* the next promise (if present) will start. The queue can only resolve one promise at a given time.
|
|
32
|
+
*
|
|
33
|
+
* @deprecated Since 2.7. The `DefaultActionDispatcher` no longer uses this queue. Kept for
|
|
34
|
+
* backwards compatibility; will be removed in a future release. New code should use
|
|
35
|
+
* {@link ActionQueue} or native async patterns instead.
|
|
32
36
|
*/
|
|
33
37
|
export class PromiseQueue<T = void> {
|
|
34
38
|
protected queue: PromiseQueueElement<T>[] = [];
|