Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GLSP-1116 Revise model loading #287

Merged
merged 3 commits into from
Sep 15, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 3 additions & 6 deletions examples/workflow-standalone/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ import {
GLSPActionDispatcher,
GLSPClient,
GLSPWebSocketProvider,
ServerMessageAction,
ServerStatusAction
MessageAction,
StatusAction
} from '@eclipse-glsp/client';
import { Container } from 'inversify';
import { join, resolve } from 'path';
Expand Down Expand Up @@ -55,10 +55,7 @@ async function initialize(connectionProvider: MessageConnection, isReconnecting
const message = `Connection to the ${id} glsp server got closed. Connection was successfully re-established.`;
const timeout = 5000;
const severity = 'WARNING';
actionDispatcher.dispatchAll([
ServerStatusAction.create(message, { severity, timeout }),
ServerMessageAction.create(message, { severity })
]);
actionDispatcher.dispatchAll([StatusAction.create(message, { severity, timeout }), MessageAction.create(message, { severity })]);
return;
}
}
Expand Down
16 changes: 13 additions & 3 deletions packages/client/src/base/action-dispatcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,15 @@
********************************************************************************/
import { inject, injectable } from 'inversify';
import { Action, ActionDispatcher, RequestAction, ResponseAction } from '~glsp-sprotty';
import { ModelInitializationConstraint } from './model-initialization-constraint';
import { ModelInitializationConstraint } from './model/model-initialization-constraint';

@injectable()
export class GLSPActionDispatcher extends ActionDispatcher {
protected readonly timeouts: Map<string, NodeJS.Timeout> = new Map();
protected initializedConstraint = false;

@inject(ModelInitializationConstraint) protected initializationConstraint: ModelInitializationConstraint;
@inject(ModelInitializationConstraint)
protected initializationConstraint: ModelInitializationConstraint;

override initialize(): Promise<void> {
return super.initialize().then(() => this.startModelInitialization());
Expand All @@ -31,7 +32,7 @@ export class GLSPActionDispatcher extends ActionDispatcher {
startModelInitialization(): void {
if (!this.initializedConstraint) {
this.logger.log(this, 'Starting model initialization mode');
this.initializationConstraint.onInitialized().then(() => this.logger.log(this, 'Model initialization completed'));
this.initializationConstraint.onInitialized(() => this.logger.log(this, 'Model initialization completed'));
this.initializedConstraint = true;
}
}
Expand All @@ -44,6 +45,15 @@ export class GLSPActionDispatcher extends ActionDispatcher {
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));
}

override dispatch(action: Action): Promise<void> {
const result = super.dispatch(action);
this.initializationConstraint.notifyDispatched(action);
Expand Down
30 changes: 30 additions & 0 deletions packages/client/src/base/action-handler-registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/********************************************************************************
* Copyright (c) 2023 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 { injectable } from 'inversify';
import { ActionHandlerRegistry } from '~glsp-sprotty';

@injectable()
export class GLSPActionHandlerRegistry extends ActionHandlerRegistry {
/**
* Retrieve a set of all action kinds for which (at least) one
* handler is registered
* @returns the set of handled action kinds
*/
getHandledActionKinds(): string[] {
return Array.from(this.elements.keys());
}
}
6 changes: 5 additions & 1 deletion packages/client/src/base/default.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
********************************************************************************/
import '@vscode/codicons/dist/codicon.css';
import {
ActionHandlerRegistry,
FeatureModule,
KeyTool,
LocationPostprocessor,
Expand All @@ -31,16 +32,17 @@ import {
} from '~glsp-sprotty';
import '../../css/glsp-sprotty.css';
import { GLSPActionDispatcher } from './action-dispatcher';
import { GLSPActionHandlerRegistry } from './action-handler-registry';
import { GLSPCommandStack } from './command-stack';
import { EditorContextService } from './editor-context-service';
import { ModifyCssFeedbackCommand } from './feedback/css-feedback';
import { FeedbackActionDispatcher } from './feedback/feedback-action-dispatcher';
import { FeedbackAwareUpdateModelCommand } from './feedback/update-model-command';
import { FocusStateChangedAction } from './focus/focus-state-change-action';
import { FocusTracker } from './focus/focus-tracker';
import { DefaultModelInitializationConstraint, ModelInitializationConstraint } from './model-initialization-constraint';
import { DiagramLoader } from './model/diagram-loader';
import { GLSPModelSource } from './model/glsp-model-source';
import { DefaultModelInitializationConstraint, ModelInitializationConstraint } from './model/model-initialization-constraint';
import { GLSPModelRegistry } from './model/model-registry';
import { SelectionClearingMouseListener } from './selection-clearing-mouse-listener';
import { SelectionService } from './selection-service';
Expand Down Expand Up @@ -91,6 +93,8 @@ export const defaultModule = new FeatureModule((bind, unbind, isBound, rebind, .
bindOrRebind(context, TYPES.ICommandStack).to(GLSPCommandStack).inSingletonScope();
bind(GLSPActionDispatcher).toSelf().inSingletonScope();
bindOrRebind(context, TYPES.IActionDispatcher).toService(GLSPActionDispatcher);
bind(GLSPActionHandlerRegistry).toSelf().inSingletonScope();
bindOrRebind(context, ActionHandlerRegistry).toService(GLSPActionHandlerRegistry);

bindAsService(context, TYPES.ModelSource, GLSPModelSource);
bind(DiagramLoader).toSelf().inSingletonScope();
Expand Down
34 changes: 23 additions & 11 deletions packages/client/src/base/model/diagram-loader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,15 @@ import {
InitializeParameters,
MaybePromise,
RequestModelAction,
ServerStatusAction,
SetModelAction,
StatusAction,
TYPES,
hasNumberProp
} from '~glsp-sprotty';
import { GLSPActionDispatcher } from '../action-dispatcher';
import { Ranked } from '../ranked';
import { GLSPModelSource } from './glsp-model-source';
import { ModelInitializationConstraint } from './model-initialization-constraint';

/**
* Configuration options for a specific GLSP diagram instance.
Expand Down Expand Up @@ -79,17 +80,24 @@ export interface IDiagramStartup extends Partial<Ranked> {
* Hook for services that should be executed after the initial model loading request (i.e. `RequestModelAction`).
* Note that this hook is invoked directly after the `RequestModelAction` has been dispatched. It does not necessarily wait
* until the client-server update roundtrip is completed. If you need to wait until the diagram is fully initialized use the
* {@link GLSPActionDispatcher.onceModelInitialized} constraint.
* {@link postModelInitialization} hook.
*/
postRequestModel?(): MaybePromise<void>;
/* Hook for services that should be executed after the diagram model is fully initialized
* (i.e. `ModelInitializationConstraint` is completed).
*/
postModelInitialization?(): MaybePromise<void>;
}

export namespace IDiagramStartup {
export function is(object: unknown): object is IDiagramStartup {
return (
AnyObject.is(object) &&
hasNumberProp(object, 'rank', true) &&
('preInitialize' in object || 'preRequestModel' in object || 'postRequestModel' in object)
('preInitialize' in object ||
'preRequestModel' in object ||
'postRequestModel' in object ||
'postModelInitialization' in object)
);
}
}
Expand Down Expand Up @@ -142,6 +150,9 @@ export class DiagramLoader {
@inject(GLSPModelSource)
protected modelSource: GLSPModelSource;

@inject(ModelInitializationConstraint)
protected modelInitializationConstraint: ModelInitializationConstraint;

@postConstruct()
protected postConstruct(): void {
this.diagramStartups.sort((a, b) => Ranked.getRank(a) - Ranked.getRank(b));
Expand All @@ -161,13 +172,15 @@ export class DiagramLoader {
},
enableNotifications: options.enableNotifications ?? true
};
await this.actionDispatcher.initialize();
// Set placeholder model until real model from server is available
await this.actionDispatcher.dispatch(SetModelAction.create(EMPTY_ROOT));
await this.invokeStartupHook('preInitialize');
await this.initialize(resolvedOptions);
await this.invokeStartupHook('preRequestModel');
await this.requestModel(resolvedOptions);
await this.invokeStartupHook('postRequestModel');
this.modelInitializationConstraint.onInitialized(() => this.invokeStartupHook('postModelInitialization'));
}

protected async invokeStartupHook(hook: keyof Omit<IDiagramStartup, 'rank'>): Promise<void> {
Expand All @@ -176,18 +189,13 @@ export class DiagramLoader {
}
}

protected requestModel(options: ResolvedDiagramLoadingOptions): Promise<void> {
const result = this.actionDispatcher.dispatch(RequestModelAction.create({ options: options.requestModelOptions }));
if (options.enableNotifications) {
this.actionDispatcher.dispatch(ServerStatusAction.create('', { severity: 'NONE' }));
}
return result;
protected async requestModel(options: ResolvedDiagramLoadingOptions): Promise<void> {
return this.actionDispatcher.dispatch(RequestModelAction.create({ options: options.requestModelOptions }));
}

protected async initialize(options: ResolvedDiagramLoadingOptions): Promise<void> {
await this.actionDispatcher.initialize();
if (options.enableNotifications) {
this.actionDispatcher.dispatch(ServerStatusAction.create('Initializing...', { severity: 'INFO' }));
this.actionDispatcher.dispatch(StatusAction.create('Initializing...', { severity: 'INFO' }));
}

const glspClient = await this.options.glspClientProvider();
Expand All @@ -197,5 +205,9 @@ export class DiagramLoader {
if (!glspClient.initializeResult) {
await glspClient.initializeServer(options.initializeParameters);
}

if (options.enableNotifications) {
this.actionDispatcher.dispatch(StatusAction.create('', { severity: 'NONE' }));
}
}
}
17 changes: 13 additions & 4 deletions packages/client/src/base/model/glsp-model-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@
import { inject, injectable, postConstruct, preDestroy } from 'inversify';
import {
Action,
ActionHandlerRegistry,
ActionMessage,
Disposable,
DisposableCollection,
Expand All @@ -28,6 +27,7 @@ import {
SModelRootSchema,
TYPES
} from '~glsp-sprotty';
import { GLSPActionHandlerRegistry } from '../action-handler-registry';
import { IDiagramOptions } from './diagram-loader';
/**
* A helper interface that allows the client to mark actions that have been received from the server.
Expand Down Expand Up @@ -91,14 +91,23 @@ export class GLSPModelSource extends ModelSource implements Disposable {
this.clientId = this.options.clientId ?? this.viewerOptions.baseDiv;
}

configure(registry: ActionHandlerRegistry, initializeResult: InitializeResult): Promise<void> {
configure(registry: GLSPActionHandlerRegistry, initializeResult: InitializeResult): Promise<void> {
const serverActions = initializeResult.serverActions[this.diagramType];
if (!serverActions || serverActions.length === 0) {
throw new Error(`No server-handled actions could be derived from the initialize result for diagramType: ${this.diagramType}!`);
}
// Retrieve all currently handled action kinds. We do this before registering the server actions
// to ensure that the array will only contain client-side handled actions
const clientActionKinds = registry.getHandledActionKinds();

serverActions.forEach(action => registry.register(action, this));
this.toDispose.push(this.glspClient!.onActionMessage(message => this.messageReceived(message), this.clientId));
return this.glspClient!.initializeClientSession({ clientSessionId: this.clientId, diagramType: this.diagramType });

return this.glspClient!.initializeClientSession({
clientSessionId: this.clientId,
clientActionKinds,
diagramType: this.diagramType
});
}

protected messageReceived(message: ActionMessage): void {
Expand All @@ -111,7 +120,7 @@ export class GLSPModelSource extends ModelSource implements Disposable {
this.actionDispatcher.dispatch(action);
}

override initialize(registry: ActionHandlerRegistry): void {
override initialize(registry: GLSPActionHandlerRegistry): void {
// Registering actions here is discouraged and it's recommended
// to implemented dedicated action handlers.
if (!this.clientId) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/********************************************************************************
* Copyright (c) 2023 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 { expect } from 'chai';
import { Container } from 'inversify';
import 'reflect-metadata';
import * as sinon from 'sinon';
import { Deferred, EMPTY_ROOT, InitializeCanvasBoundsAction, SetModelAction, UpdateModelAction } from '~glsp-sprotty';
import { DefaultModelInitializationConstraint, ModelInitializationConstraint } from './model-initialization-constraint';
const sandbox = sinon.createSandbox();
const container = new Container();
let constraint: ModelInitializationConstraint;
// eslint-disable-next-line @typescript-eslint/no-empty-function
const listener = sandbox.spy((): void => {});

describe('DefaultModelInitializationConstraint', () => {
beforeEach(() => {
constraint = container.resolve(DefaultModelInitializationConstraint);
sandbox.reset();
});
it('should complete after dispatching non empty SetModelAction and `InitializeCanvasBoundsAction`', () => {
expect(constraint.isCompleted).to.be.false;
constraint.notifyDispatched(SetModelAction.create({ id: 'model', type: 'graph' }));
expect(constraint.isCompleted).to.be.false;
constraint.notifyDispatched({ kind: InitializeCanvasBoundsAction.KIND });
expect(constraint.isCompleted).to.be.true;
});
it('should complete after dispatching non empty UpdateModelAction and `InitializeCanvasBoundsAction`', () => {
expect(constraint.isCompleted).to.be.false;
constraint.notifyDispatched(UpdateModelAction.create({ id: 'model', type: 'graph' }));
expect(constraint.isCompleted).to.be.false;
constraint.notifyDispatched({ kind: InitializeCanvasBoundsAction.KIND });
expect(constraint.isCompleted).to.be.true;
});
it('should note complete after dispatching empty SetModelAction and `InitializeCanvasBoundsAction` ', () => {
expect(constraint.isCompleted).to.be.false;
constraint.notifyDispatched(SetModelAction.create(EMPTY_ROOT));
expect(constraint.isCompleted).to.be.false;
constraint.notifyDispatched({ kind: InitializeCanvasBoundsAction.KIND });
expect(constraint.isCompleted).to.be.false;
});
it('should note complete after dispatching empty UpdateModelAction and `InitializeCanvasBoundsAction ', () => {
expect(constraint.isCompleted).to.be.false;
constraint.notifyDispatched(UpdateModelAction.create(EMPTY_ROOT));
expect(constraint.isCompleted).to.be.false;
constraint.notifyDispatched({ kind: InitializeCanvasBoundsAction.KIND });
expect(constraint.isCompleted).to.be.false;
});
describe('onInitialized', () => {
it('returned promise should resolve once the constraint is initialized', async () => {
const initializeDeferred = new Deferred<void>();
const initializePromise = constraint.onInitialized();
initializePromise.then(() => initializeDeferred.resolve());
expect(initializeDeferred.state).to.be.equal('unresolved');
// Directly trigger the completion method simplify test logic
constraint['setCompleted']();
// Short delay of test execution to ensure that the deferred state is updated.
await new Promise(resolve => setTimeout(resolve, 5));
expect(initializeDeferred.state).to.be.equal('resolved');
});
it('registered listener should be invoked once the constraint is initialized', () => {
constraint.onInitialized(listener);
expect(listener.called).to.be.false;
// Directly trigger the completion method simplify test logic
constraint['setCompleted']();
expect(listener.called).to.be.true;
});
it('registered listener should be invoked directly on registration if the constraint is already initialized', () => {
// Directly trigger the completion method simplify test logic
constraint['setCompleted']();
constraint.onInitialized(listener);
expect(listener.called).to.be.true;
});
it('Disposed listener should not be invoked once the constraint is initialized', () => {
const toDispose = constraint.onInitialized(listener);
expect(listener.called).to.be.false;
toDispose.dispose();
// Directly trigger the completion method simplify test logic
constraint['setCompleted']();
expect(listener.called).to.be.false;
});
});
});
Loading