Communication with Actions
Interactions between the client and server are based on an event-based protocol. Sprotty makes a distinction between two types of events: Actions
and Commands
. Actions are used to communicate between the client and the server, while commands are used to apply changes to the diagram on the client side.
Actions are used to communicate between the client and the server in a bidirectional manner. They are plain JSON serializable objects so that they can easily be passed through all kinds of APIs. They can be distinguished through their KIND
string property. For a list of available actions, see this reference page
We can further distinguish between three types of actions:
This is a “fire and forget” event. It is sent from the client to the server, or vice versa, to notify the other side of a certain event. The other side can react to this event, but it does not need to send a response.
export interface MyAction extends Action {
kind: typeof MyAction.KIND
}
export namespace MyAction {
export const KIND = 'my-action'
export function create(): MyAction {
return { kind: KIND }
}
}
// for dispatching the action
dispatcher.dispatch(MyAction.create())
// or
dispatcher.dispatch({ kind: MyAction.KIND })
This type of action is sent to request a certain piece of information. The other side is expected to respond with a ResponseAction
.
export interface MyRequestAction extends RequestAction {
kind: typeof MyRequestAction.KIND
}
export namespace MyRequestAction {
export const KIND = 'my-request-action'
export function create(): MyRequestAction {
return { kind: KIND }
}
}
// for dispatching the action
const response = dispatcher.request<MyResponseAction>(MyRequestAction.create())
// or
const response = dispatcher.request({ kind: MyRequestAction.KIND })
This type of action is sent as a response to a RequestAction
. It contains the requested information.
export interface MyResponseAction extends ResponseAction {
kind: typeof MyResponseAction.KIND
}
export namespace MyResponseAction {
export const KIND = 'my-response-action'
export function create(): MyResponseAction {
return { kind: KIND }
}
}
Commands are used to apply changes to the diagram on the client side. They are triggered by their associated action.
export class MyCommand extends Command {
static KIND = 'my-command'
constructor(@inject(TYPES.Action) protected readonly action: MyAction) {
super()
}
execute(context: CommandExecutionContext): CommandReturn {
// apply changes to the diagram
}
undo(context: CommandExecutionContext): CommandReturn {
// revert changes to the diagram
}
redo(context: CommandExecutionContext): CommandReturn {
// re-apply changes to the diagram
}
// A `CommandReturn` is a SModelRootImpl, or a Promise<SModelRootImpl>, or a `CommandResult`(i.e. { model: SModelRootImpl, modelChanged: boolean, cause?: Action })
}
Commands need to be registered in the dependency injection container.
const myContainer = new ContainerModule((bind, unbind, isBound, rebind) => {
...
const context = { bind, isBound }
configureCommand(context, MyCommand)
})
A ModelSource is responsible for handling actions. A local ModelSource is used to handle actions on the client side, while a remote ModelSource is used to handle actions on the server side.
Every action that the designated ModelSource is supposed to handle needs to be registered inside the initialize
method, and the method containing the logic for handling the action needs to be implemented in the ModelSource and selected via a switch in the handle
method.
// the class can extend LocalModelSource, DiagramServerProxy, or any derivative of ModelSource
export class MyModelSource extends LocalModelSource {
initialize(registry: ActionHandlerRegistry): void {
super.initialize(registry)
registry.register(MyAction.KIND, this)
}
handle(action: Action): void {
switch (action.kind) {
case MyAction.KIND:
this.handleMyAction(action as MyAction)
break
default:
super.handle(action)
}
}
protected handleMyAction(action: MyAction): void {
// handle the action
}
}
The ActionDispatcher collects actions, converts them to command if necessary, and dispatches them to their respective handlers.
export class ActionDispatcher implements IActionDispatcher {
// dispatch a single action
dispatch(action: Action): Promise<void> {
...
}
// dispatch multiple actions
dispatchAll(actions: Action[]): Promise<void> {
...
}
// dispatch a RequestAction
request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res> {
...
}
}
An action (i.e. “fire and forget” event) is sent to the action dispatcher. The action dispatcher then dispatches the action to its respective handler. The handler can then react to the action and a Command may or may not be triggered on the client. Here we will look at how an Action sent from the Client is handled by the ActionDispatcher and its respective ActionHandler on the server side:
sequenceDiagram participant C as Client participant AD as ActionDispatcher participant AH as ActionHandler C ->> AD: An Action is sent to the ActionDispatcher AD ->> AH: The Action is forwarded to the corresponding ActionHandler AH -->> C: The Action may trigger a Command
This is the simplest scenario. In reality, the ActionHandler may also send another Action to the ActionDispatcher, creating an Action chain. In most cases, the ActionHandler will apply some changes to the model and then send another action to update the model on the client side.
A RequestAction is sent from the client to the server. The server is expected to produce a ResponseAction as a response to the RequestAction. Here we will look at how a RequestAction sent from the Client is handled by the ActionDispatcher and its respective handler on the server side:
sequenceDiagram participant C as Client participant AD as ActionDispatcher participant RAH as RequestActionHandler participant RespAH as ResponseActionHandler C ->> AD: A RequestAction is sent AD ->> RAH: The RequestAction is forwarded to the corresponding ActionHandler RAH ->> AD: A ResponseAction is sent AD ->> RespAH: The ResponseAction is forwarded to the corresponding ActionHandler RespAH -->> C: The Action may trigger a Command