sprotty
Version:
A next-gen framework for graphical views
189 lines (172 loc) • 8.23 kB
text/typescript
/********************************************************************************
* Copyright (c) 2017-2018 TypeFox and others.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v. 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0.
*
* This Source Code may also be made available under the following Secondary
* Licenses when the conditions for such availability set forth in the Eclipse
* Public License v. 2.0 are satisfied: GNU General Public License, version 2
* with the GNU Classpath Exception which is available at
* https://www.gnu.org/software/classpath/license.html.
*
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { inject, injectable } from 'inversify';
import {
Action, isAction, isRequestAction, isResponseAction, RedoAction, RejectAction, RequestAction,
ResponseAction, SetModelAction, setRequestContext, UndoAction
} from 'sprotty-protocol/lib/actions';
import { Deferred } from 'sprotty-protocol/lib/utils/async';
import { TYPES } from '../types';
import { ILogger } from '../../utils/logging';
import { EMPTY_ROOT } from '../model/smodel-factory';
import { ICommandStack } from '../commands/command-stack';
import { AnimationFrameSyncer } from '../animations/animation-frame-syncer';
import { ActionHandlerRegistry } from './action-handler';
import { IDiagramLocker } from './diagram-locker';
export interface IActionDispatcher {
dispatch(action: Action): Promise<void>
dispatchAll(actions: Action[]): Promise<void>
request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res>
}
// This code should be used only in the client part of a Sprotty application.
// We set the request context to 'client' to avoid collisions with requests created by the server.
setRequestContext('client');
/**
* Collects actions, converts them to commands and dispatches them.
* Also acts as the proxy to model sources such as diagram servers.
*/
export class ActionDispatcher implements IActionDispatcher {
protected actionHandlerRegistryProvider: () => Promise<ActionHandlerRegistry>;
protected commandStack: ICommandStack;
protected logger: ILogger;
protected syncer: AnimationFrameSyncer;
protected diagramLocker: IDiagramLocker;
protected actionHandlerRegistry: ActionHandlerRegistry;
protected initialized: Promise<void> | undefined;
protected blockUntil?: (action: Action) => boolean;
protected postponedActions: PostponedAction[] = [];
protected readonly requests: Map<string, Deferred<ResponseAction>> = new Map();
initialize(): Promise<void> {
if (!this.initialized) {
this.initialized = this.actionHandlerRegistryProvider().then(registry => {
this.actionHandlerRegistry = registry;
this.handleAction(SetModelAction.create(EMPTY_ROOT)).catch(() => { /* Logged in handleAction method */ });
});
}
return this.initialized;
}
/**
* Dispatch an action by querying all handlers that are registered for its kind.
* The returned promise is resolved when all handler results (commands or actions)
* have been processed.
*/
dispatch(action: Action): Promise<void> {
return this.initialize().then(() => {
if (this.blockUntil !== undefined) {
return this.handleBlocked(action, this.blockUntil);
} else if (this.diagramLocker.isAllowed(action)) {
return this.handleAction(action);
}
return undefined;
});
}
/**
* Calls `dispatch` on every action in the given array. The returned promise
* is resolved when the promises of all `dispatch` calls have been resolved.
*/
dispatchAll(actions: Action[]): Promise<void> {
return Promise.all(actions.map(action => this.dispatch(action))) as Promise<any>;
}
/**
* Dispatch a request. The returned promise is resolved when a response with matching
* identifier is dispatched. That response is _not_ passed to the registered action
* handlers. Instead, it is the responsibility of the caller of this method to handle
* the response properly. For example, it can be sent to the registered handlers by
* passing it again to the `dispatch` method.
*/
request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res> {
if (!action.requestId) {
return Promise.reject(new Error('Request without requestId'));
}
const deferred = new Deferred<Res>();
this.requests.set(action.requestId, deferred);
this.dispatch(action).catch(() => { /* Logged in handleAction method */ });
return deferred.promise;
}
protected handleAction(action: Action): Promise<void> {
if (action.kind === UndoAction.KIND) {
return this.commandStack.undo().then(() => {});
}
if (action.kind === RedoAction.KIND) {
return this.commandStack.redo().then(() => {});
}
if (isResponseAction(action)) {
const deferred = this.requests.get(action.responseId);
if (deferred !== undefined) {
this.requests.delete(action.responseId);
if (action.kind === RejectAction.KIND) {
const rejectAction = action as RejectAction;
deferred.reject(new Error(rejectAction.message));
this.logger.warn(this, `Request with id ${action.responseId} failed.`,
rejectAction.message, rejectAction.detail);
} else {
deferred.resolve(action);
}
return Promise.resolve();
}
this.logger.log(this, 'No matching request for response', action);
}
const handlers = this.actionHandlerRegistry.get(action.kind);
if (handlers.length === 0) {
this.logger.warn(this, 'Missing handler for action', action);
const error = new Error(`Missing handler for action '${action.kind}'`);
if (isRequestAction(action)) {
const deferred = this.requests.get(action.requestId);
if (deferred !== undefined) {
this.requests.delete(action.requestId);
deferred.reject(error);
}
}
return Promise.reject(error);
}
this.logger.log(this, 'Handle', action);
const promises: Promise<any>[] = [];
for (const handler of handlers) {
const result = handler.handle(action);
if (isAction(result)) {
promises.push(this.dispatch(result));
} else if (result !== undefined) {
promises.push(this.commandStack.execute(result));
this.blockUntil = result.blockUntil;
}
}
return Promise.all(promises) as Promise<any>;
}
protected handleBlocked(action: Action, predicate: (action: Action) => boolean): Promise<void> {
if (predicate(action)) {
this.blockUntil = undefined;
const result = this.handleAction(action);
const actions = this.postponedActions;
this.postponedActions = [];
for (const a of actions) {
this.dispatch(a.action).then(a.resolve, a.reject);
}
return result;
} else {
this.logger.log(this, 'Action is postponed due to block condition', action);
return new Promise((resolve, reject) => {
this.postponedActions.push({ action, resolve, reject });
});
}
}
}
export interface PostponedAction {
action: Action
resolve: () => void
reject: (reason: any) => void
}
export type IActionDispatcherProvider = () => Promise<IActionDispatcher>;