@eclipse-glsp/client
Version:
A sprotty-based client for GLSP
200 lines (177 loc) • 8.11 kB
text/typescript
/********************************************************************************
* Copyright (c) 2019-2024 EclipseSource 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 {
Action,
ActionDispatcher,
ActionHandlerRegistry,
Deferred,
EMPTY_ROOT,
GModelRoot,
IActionDispatcher,
RequestAction,
ResponseAction,
SetModelAction,
TYPES
} from '@eclipse-glsp/sprotty';
import { inject, injectable } from 'inversify';
import { GLSPActionHandlerRegistry } from './action-handler-registry';
import { IGModelRootListener } from './editor-context-service';
import { OptionalAction } from './model/glsp-model-source';
import { ModelInitializationConstraint } from './model/model-initialization-constraint';
export class GLSPActionDispatcher extends ActionDispatcher implements IGModelRootListener, IActionDispatcher {
protected readonly timeouts: Map<string, NodeJS.Timeout> = new Map();
protected initializedConstraint = false;
protected initializationConstraint: ModelInitializationConstraint;
protected override actionHandlerRegistry: ActionHandlerRegistry;
/** @deprecated No longer in used. The {@link ActionHandlerRegistry} is now directly injected */
// eslint-disable-next-line deprecation/deprecation
protected override actionHandlerRegistryProvider: () => Promise<ActionHandlerRegistry>;
protected postUpdateQueue: Action[] = [];
protected initializeDeferred = new Deferred<void>();
override initialize(): Promise<void> {
if (!this.initialized) {
this.initialized = this.initializeDeferred.promise;
this.doInitialize();
}
return this.initialized;
}
protected async doInitialize(): Promise<void> {
try {
if (this.actionHandlerRegistry instanceof GLSPActionHandlerRegistry) {
this.actionHandlerRegistry.initialize();
}
this.handleAction(SetModelAction.create(EMPTY_ROOT)).catch(() => {
/* Logged in handleAction method */
});
this.startModelInitialization();
this.initializeDeferred.resolve();
} catch (error) {
this.initializeDeferred.reject(error);
}
}
protected startModelInitialization(): void {
if (!this.initializedConstraint) {
this.logger.log(this, 'Starting model initialization mode');
this.initializationConstraint.onInitialized(() => this.logger.log(this, 'Model initialization completed'));
this.initializedConstraint = true;
}
}
onceModelInitialized(): Promise<void> {
return this.initializationConstraint.onInitialized();
}
hasHandler(action: Action): boolean {
return this.actionHandlerRegistry.get(action.kind).length > 0;
}
/**
* Processes all given actions, by dispatching them to the corresponding handlers, after the model initialization is completed.
*
* @param actions The actions that should be dispatched after the model initialization
*/
dispatchOnceModelInitialized(...actions: Action[]): void {
this.initializationConstraint.onInitialized(() => this.dispatchAll(actions));
}
/**
* Processes all given actions, by dispatching them to the corresponding handlers, after the next model update.
* The given actions are queued until the next model update cycle has been completed i.e.
* the `EditorContextService.onModelRootChanged` event is triggered.
*
* @param actions The actions that should be dispatched after the next model update
*/
dispatchAfterNextUpdate(...actions: Action[]): void {
this.postUpdateQueue.push(...actions);
}
modelRootChanged(_root: Readonly<GModelRoot>): void {
if (this.postUpdateQueue.length === 0) {
return;
}
const toDispatch = [...this.postUpdateQueue];
this.postUpdateQueue = [];
this.dispatchAll(toDispatch);
}
override async dispatch(action: Action): Promise<void> {
const result = await super.dispatch(action);
this.initializationConstraint.notifyDispatched(action);
return result;
}
protected override handleAction(action: Action): Promise<void> {
if (ResponseAction.hasValidResponseId(action)) {
// clear timeout
const timeout = this.timeouts.get(action.responseId);
if (timeout !== undefined) {
clearTimeout(timeout);
this.timeouts.delete(action.responseId);
}
// Check if we have a pending request for the response.
// If not the we clear the responseId => action will be dispatched normally
const deferred = this.requests.get(action.responseId);
if (deferred === undefined) {
action.responseId = '';
}
}
if (!this.hasHandler(action) && OptionalAction.is(action)) {
return Promise.resolve();
}
return super.handleAction(action);
}
override request<Res extends ResponseAction>(action: RequestAction<Res>): Promise<Res> {
if (!action.requestId && action.requestId === '') {
// No request id has been specified. So we use a generated one.
action.requestId = RequestAction.generateRequestId();
}
return super.request(action);
}
/**
* Dispatch a request and waits for a response until the timeout given in `timeoutMs` has
* been reached. The returned promise is resolved when a response with matching identifier
* is dispatched or when the timeout has been reached. 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.
* If `rejectOnTimeout` is set to false (default) the returned promise will be resolved with
* no value, otherwise it will be rejected.
*/
requestUntil<Res extends ResponseAction>(
action: RequestAction<Res>,
timeoutMs = 2000,
rejectOnTimeout = false
): Promise<Res | undefined> {
if (!action.requestId && action.requestId === '') {
// No request id has been specified. So we use a generated one.
action.requestId = RequestAction.generateRequestId();
}
const requestId = action.requestId;
const timeout = setTimeout(() => {
const deferred = this.requests.get(requestId);
if (deferred !== undefined) {
// cleanup
clearTimeout(timeout);
this.requests.delete(requestId);
const notification = 'Request ' + requestId + ' (' + action + ') time out after ' + timeoutMs + 'ms.';
if (rejectOnTimeout) {
deferred.reject(notification);
} else {
this.logger.info(this, notification);
deferred.resolve();
}
}
}, timeoutMs);
this.timeouts.set(requestId, timeout);
return super.request<Res>(action);
}
}