Skip to content

Commit 609c2ba

Browse files
Support asynchronous action handlers on client-side (#457)
Fixes eclipse-glsp/glsp#1609
1 parent 2fe0c5e commit 609c2ba

File tree

5 files changed

+180
-31
lines changed

5 files changed

+180
-31
lines changed

packages/client/src/base/action-dispatcher.ts

Lines changed: 88 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@
1616
import {
1717
Action,
1818
ActionDispatcher,
19+
ActionHandler,
1920
ActionHandlerRegistry,
2021
Deferred,
2122
EMPTY_ROOT,
2223
GModelRoot,
2324
IActionDispatcher,
25+
RejectAction,
2426
RequestAction,
2527
ResponseAction,
2628
SetModelAction,
2729
TYPES
2830
} from '@eclipse-glsp/sprotty';
2931
import { inject, injectable } from 'inversify';
32+
import * as sprotty from 'sprotty-protocol/lib/actions';
3033
import { GLSPActionHandlerRegistry } from './action-handler-registry';
3134
import { IGModelRootListener } from './editor-context-service';
3235
import { OptionalAction } from './model/glsp-model-source';
@@ -125,26 +128,96 @@ export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRoo
125128
return result;
126129
}
127130

128-
protected override handleAction(action: Action): Promise<void> {
131+
protected override async handleAction(action: Action): Promise<void> {
129132
if (ResponseAction.hasValidResponseId(action)) {
130-
// clear timeout
131-
const timeout = this.timeouts.get(action.responseId);
132-
if (timeout !== undefined) {
133-
clearTimeout(timeout);
134-
this.timeouts.delete(action.responseId);
135-
}
133+
return this.handleResponseAction(action);
134+
}
135+
if (OptionalAction.is(action) && !this.hasHandler(action)) {
136+
return Promise.resolve();
137+
}
138+
if (sprotty.UndoAction.KIND === action.kind) {
139+
return this.handleUndoAction(action);
140+
}
141+
if (sprotty.RedoAction.KIND === action.kind) {
142+
return this.handleRedoAction(action);
143+
}
144+
return this.doHandleAction(action);
145+
}
146+
147+
protected handleResponseAction(action: ResponseAction): Promise<void> {
148+
// clear any pending timeout
149+
const timeout = this.timeouts.get(action.responseId);
150+
if (timeout !== undefined) {
151+
clearTimeout(timeout);
152+
this.timeouts.delete(action.responseId);
153+
}
136154

137-
// Check if we have a pending request for the response.
138-
// If not the we clear the responseId => action will be dispatched normally
139-
const deferred = this.requests.get(action.responseId);
140-
if (deferred === undefined) {
141-
action.responseId = '';
155+
// check for matching request
156+
const request = this.requests.get(action.responseId);
157+
if (!request) {
158+
// treat no matching request by dispatching action normally
159+
this.logger.log(this, 'No matching request for response, dispatch normally', action);
160+
action.responseId = '';
161+
return this.handleAction(action);
162+
}
163+
164+
this.requests.delete(action.responseId);
165+
if (RejectAction.is(action)) {
166+
// translation reject action to request rejection
167+
request.reject(new Error(action.message));
168+
this.logger.warn(this, `Request with id ${action.responseId} failed.`, action.message, action.detail);
169+
} else {
170+
request.resolve(action);
171+
}
172+
return Promise.resolve();
173+
}
174+
175+
protected handleUndoAction(action: Action): Promise<void> {
176+
return this.commandStack.undo().then(() => {});
177+
}
178+
179+
protected handleRedoAction(action: Action): Promise<void> {
180+
return this.commandStack.redo().then(() => {});
181+
}
182+
183+
protected async doHandleAction(action: Action): Promise<void> {
184+
const handlers = this.actionHandlerRegistry.get(action.kind);
185+
if (handlers.length === 0) {
186+
return this.handleActionWithoutHandler(action);
187+
} else {
188+
return this.handlerActionWithHandler(action, handlers);
189+
}
190+
}
191+
192+
protected handleActionWithoutHandler(action: Action): Promise<void> {
193+
this.logger.warn(this, 'Missing handler for action', action);
194+
const error = new Error(`Missing handler for action '${action.kind}'`);
195+
if (RequestAction.is(action)) {
196+
const request = this.requests.get(action.requestId);
197+
if (request !== undefined) {
198+
this.requests.delete(action.requestId);
199+
request.reject(error);
142200
}
143201
}
144-
if (!this.hasHandler(action) && OptionalAction.is(action)) {
145-
return Promise.resolve();
202+
return Promise.reject(error);
203+
}
204+
205+
protected async handlerActionWithHandler(action: Action, handlers: ActionHandler[]): Promise<any> {
206+
this.logger.log(this, 'Handle', action);
207+
const handlerResults: Promise<any>[] = [];
208+
for (const handler of handlers) {
209+
const handlerResult = Promise.resolve(handler.handle(action)).then<any>(result => {
210+
if (Action.is(result)) {
211+
return this.dispatch(result);
212+
} else if (result !== undefined) {
213+
this.blockUntil = result.blockUntil;
214+
return this.commandStack.execute(result);
215+
}
216+
return undefined;
217+
});
218+
handlerResults.push(handlerResult);
146219
}
147-
return super.handleAction(action);
220+
return Promise.all(handlerResults) as Promise<any>;
148221
}
149222

150223
override request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res> {
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
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 { Action } from '@eclipse-glsp/protocol';
17+
import { injectable, interfaces, multiInject, optional } from 'inversify';
18+
import { ICommand, MultiInstanceRegistry, TYPES } from 'sprotty';
19+
import {
20+
ActionHandlerRegistry as SActionHandlerRegistry,
21+
IActionHandler as SIActionHandler,
22+
configureActionHandler as sconfigureActionHandler
23+
} from 'sprotty/lib/base/actions/action-handler';
24+
25+
export type SyncActionHandleResult = ICommand | Action | void;
26+
export type ASyncActionHandleResult = Promise<SyncActionHandleResult>;
27+
export type ActionHandleResult = SyncActionHandleResult | ASyncActionHandleResult;
28+
29+
/**
30+
* An action handler accepts an action and reacts to it by returning either a command to be
31+
* executed, or another action to be dispatched.
32+
*/
33+
export interface IActionHandler {
34+
handle(action: Action): ActionHandleResult;
35+
}
36+
37+
export interface ActionHandlerRegistration {
38+
actionKind: string;
39+
factory: () => IActionHandler;
40+
}
41+
42+
export interface IActionHandlerInitializer {
43+
initialize(registry: ActionHandlerRegistry): void;
44+
}
45+
46+
export type ActionHandler = IActionHandler & SIActionHandler;
47+
48+
@injectable()
49+
export class ActionHandlerRegistry extends MultiInstanceRegistry<ActionHandler> implements SActionHandlerRegistry {
50+
constructor(
51+
@multiInject(TYPES.ActionHandlerRegistration) @optional() registrations: ActionHandlerRegistration[],
52+
@multiInject(TYPES.IActionHandlerInitializer) @optional() initializers: IActionHandlerInitializer[]
53+
) {
54+
super();
55+
registrations.forEach(registration => this.register(registration.actionKind, registration.factory()));
56+
initializers.forEach(initializer => this.initializeActionHandler(initializer));
57+
}
58+
59+
override register(key: string, instance: IActionHandler): void;
60+
override register(key: string, instance: SIActionHandler): void;
61+
override register(key: string, instance: ActionHandler): void {
62+
super.register(key, instance);
63+
}
64+
65+
initializeActionHandler(initializer: IActionHandlerInitializer): void {
66+
initializer.initialize(this);
67+
}
68+
}
69+
70+
export function configureActionHandler(
71+
context: { bind: interfaces.Bind; isBound: interfaces.IsBound },
72+
kind: string,
73+
constr: interfaces.ServiceIdentifier<IActionHandler>
74+
): void {
75+
sconfigureActionHandler(context, kind, constr);
76+
}
77+
78+
/**
79+
* Utility function to register an action handler for an action kind.
80+
*/
81+
export function onAction(
82+
context: { bind: interfaces.Bind; isBound: interfaces.IsBound },
83+
kind: string,
84+
handle: (action: Action) => ReturnType<IActionHandler['handle']>
85+
): void {
86+
context.bind(TYPES.ActionHandlerRegistration).toConstantValue({
87+
actionKind: kind,
88+
factory: () => ({ handle })
89+
});
90+
}

packages/glsp-sprotty/src/api-override.ts

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import {
2222
SModelRootImpl as GModelRoot,
2323
ICommand,
2424
IActionDispatcher as SIActionDispatcher,
25-
IActionHandler as SIActionHandler,
2625
IButtonHandler as SIButtonHandler,
2726
ICommandPaletteActionProvider as SICommandPaletteActionProvider,
2827
ICommandStack as SICommandStack,
@@ -43,14 +42,6 @@ import {
4342
*
4443
*/
4544

46-
/**
47-
* An action handler accepts an action and reacts to it by returning either a command to be
48-
* executed, or another action to be dispatched.
49-
*/
50-
export interface IActionHandler extends SIActionHandler {
51-
handle(action: Action): ICommand | Action | void;
52-
}
53-
5445
export interface IButtonHandler extends SIButtonHandler {
5546
buttonPressed(button: GButton): (Action | Promise<Action>)[];
5647
}

packages/glsp-sprotty/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
*
1414
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
1515
********************************************************************************/
16+
export * from './action-handler-override';
1617
export * from './api-override';
1718
export * from './feature-modules';
1819
export * from './layout-override';

packages/glsp-sprotty/src/re-exports.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,13 +29,7 @@ export * from '@eclipse-glsp/protocol/lib/di';
2929
// export * from 'sprotty/lib/base/actions/action';
3030
// Exclude IActionDispatcher and IActionDispatcherProvider. Exported via api-override module instead
3131
export { ActionDispatcher, PostponedAction } from 'sprotty/lib/base/actions/action-dispatcher';
32-
export {
33-
ActionHandlerRegistration,
34-
ActionHandlerRegistry,
35-
configureActionHandler,
36-
IActionHandlerInitializer,
37-
onAction
38-
} from 'sprotty/lib/base/actions/action-handler';
32+
// export * from 'sprotty/lib/base/actions/action-handler';
3933
export * from 'sprotty/lib/base/actions/diagram-locker';
4034

4135
export * from 'sprotty/lib/base/animations/animation';

0 commit comments

Comments
 (0)