Skip to content

Commit

Permalink
GLSP-960 Improve Diagram container configuration
Browse files Browse the repository at this point in the history
- Make import configuration constants available in DI container via `IDiagramOptions`.
  This includes diagramType, clientId, sourceUri and glspClient.
- Introduce `DiagramLoader` as new central component responsible for configuring the glsp client and dispatching the intial model loading request
- Introduce `IDiagramStartup` services that can hook into the diagram loader lifecycle (GLSP-587)
- This means that feature modules are now self containted and can dispatch their initial actions, activate UI extensions on startup etc. without having to modifiy the `GLSPDiagram` (or similiar) componenent.

-Add `onServerInitialized` event + tests to `GLSPClient`

Part of eclipse-glsp/glsp#960
Part of eclipse-glsp/glsp#587
  • Loading branch information
tortmayr committed Aug 8, 2023
1 parent cc57a50 commit 0be9853
Show file tree
Hide file tree
Showing 20 changed files with 355 additions and 136 deletions.
25 changes: 14 additions & 11 deletions examples/workflow-glsp/src/workflow-diagram-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
GLSPGraph,
GLSPProjectionView,
GridSnapper,
IDiagramOptions,
LogLevel,
RectangularNodeView,
RevealNamedElementActionProvider,
Expand All @@ -38,9 +39,9 @@ import {
bindOrRebind,
configureDefaultModelElements,
configureModelElement,
createDiagramOptionsModule,
editLabelFeature,
initializeDiagramContainer,
overrideViewerOptions
initializeDiagramContainer
} from '@eclipse-glsp/client';
import 'balloon-css/balloon.min.css';
import { Container, ContainerModule } from 'inversify';
Expand Down Expand Up @@ -78,19 +79,21 @@ export const workflowDiagramModule = new ContainerModule((bind, unbind, isBound,
configureModelElement(context, 'struct', SCompartment, StructureCompartmentView);
});

export function createWorkflowDiagramContainer(widgetId: string, ...containerConfiguration: ContainerConfiguration): Container {
return initializeWorkflowDiagramContainer(new Container(), widgetId, ...containerConfiguration);
export function createWorkflowDiagramContainer(options: IDiagramOptions, ...containerConfiguration: ContainerConfiguration): Container {
return initializeWorkflowDiagramContainer(new Container(), options, ...containerConfiguration);
}

export function initializeWorkflowDiagramContainer(
container: Container,
widgetId: string,
options: IDiagramOptions,
...containerConfiguration: ContainerConfiguration
): Container {
initializeDiagramContainer(container, workflowDiagramModule, directTaskEditor, accessibilityModule, ...containerConfiguration);
overrideViewerOptions(container, {
baseDiv: widgetId,
hiddenDiv: widgetId + '_hidden'
});
return container;
return initializeDiagramContainer(
container,
createDiagramOptionsModule(options),
workflowDiagramModule,
directTaskEditor,
accessibilityModule,
...containerConfiguration
);
}
57 changes: 12 additions & 45 deletions examples/workflow-standalone/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,15 @@
import 'reflect-metadata';

import {
ApplicationIdProvider,
BaseJsonrpcGLSPClient,
EnableToolPaletteAction,
DiagramLoader,
GLSPActionDispatcher,
GLSPClient,
GLSPModelSource,
GLSPWebSocketProvider,
RequestModelAction,
RequestTypeHintsAction,
ServerMessageAction,
ServerStatusAction,
SetUIExtensionVisibilityAction,
StatusOverlay,
configureServerActions
ServerStatusAction
} from '@eclipse-glsp/client';
import { Container } from 'inversify';
import { join, resolve } from 'path';
import { MessageConnection } from 'vscode-jsonrpc';
import createContainer from './di.config';
Expand All @@ -41,45 +35,21 @@ const diagramType = 'workflow-diagram';
const loc = window.location.pathname;
const currentDir = loc.substring(0, loc.lastIndexOf('/'));
const examplePath = resolve(join(currentDir, '../app/example1.wf'));
const clientId = ApplicationIdProvider.get() + '_' + examplePath;
const clientId = 'sprotty';

const webSocketUrl = `ws://localhost:${port}/${id}`;

let container = createContainer();
let diagramServer = container.get(GLSPModelSource);

let glspClient: GLSPClient;
let container: Container;
const wsProvider = new GLSPWebSocketProvider(webSocketUrl);
wsProvider.listen({ onConnection: initialize, onReconnect: reconnect, logger: console });

async function initialize(connectionProvider: MessageConnection, isReconnecting = false): Promise<void> {
const actionDispatcher: GLSPActionDispatcher = container.get(GLSPActionDispatcher);

await actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: StatusOverlay.ID, visible: true }));
await actionDispatcher.dispatch(ServerStatusAction.create('Initializing...', { severity: 'INFO' }));
const client = new BaseJsonrpcGLSPClient({ id, connectionProvider });

await diagramServer.connect(client, clientId);
const result = await client.initializeServer({
applicationId: ApplicationIdProvider.get(),
protocolVersion: GLSPClient.protocolVersion
});
actionDispatcher.dispatch(ServerStatusAction.create('', { severity: 'NONE' }));
await configureServerActions(result, diagramType, container);

await client.initializeClientSession({ clientSessionId: diagramServer.clientId, diagramType });

actionDispatcher.dispatch(SetUIExtensionVisibilityAction.create({ extensionId: StatusOverlay.ID, visible: true }));
actionDispatcher.dispatch(EnableToolPaletteAction.create());
actionDispatcher.dispatch(
RequestModelAction.create({
options: {
sourceUri: `file://${examplePath}`,
diagramType,
isReconnecting
}
})
);
actionDispatcher.dispatch(RequestTypeHintsAction.create());
glspClient = new BaseJsonrpcGLSPClient({ id, connectionProvider });
container = createContainer({ clientId, diagramType, glspClient, sourceUri: examplePath });
const actionDispatcher = container.get(GLSPActionDispatcher);
const diagramLoader = container.get(DiagramLoader);
await diagramLoader.load({ isReconnecting });

if (isReconnecting) {
const message = `Connection to the ${id} glsp server got closed. Connection was successfully re-established.`;
Expand All @@ -94,9 +64,6 @@ async function initialize(connectionProvider: MessageConnection, isReconnecting
}

async function reconnect(connectionProvider: MessageConnection): Promise<void> {
container = createContainer();
diagramServer = container.get(GLSPModelSource);
diagramServer.clientId = clientId;

glspClient.stop();
initialize(connectionProvider, true /* isReconnecting */);
}
6 changes: 3 additions & 3 deletions examples/workflow-standalone/src/di.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,11 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import { createWorkflowDiagramContainer } from '@eclipse-glsp-examples/workflow-glsp';
import { bindOrRebind, ConsoleLogger, LogLevel, STANDALONE_MODULE_CONFIG, TYPES } from '@eclipse-glsp/client';
import { bindOrRebind, ConsoleLogger, IDiagramOptions, LogLevel, STANDALONE_MODULE_CONFIG, TYPES } from '@eclipse-glsp/client';
import { Container } from 'inversify';
import '../css/diagram.css';
export default function createContainer(): Container {
const container = createWorkflowDiagramContainer('sprotty', STANDALONE_MODULE_CONFIG);
export default function createContainer(options: IDiagramOptions): Container {
const container = createWorkflowDiagramContainer(options, STANDALONE_MODULE_CONFIG);
bindOrRebind(container, TYPES.ILogger).to(ConsoleLogger).inSingletonScope();
bindOrRebind(container, TYPES.LogLevel).toConstantValue(LogLevel.warn);
container.bind(TYPES.IMarqueeBehavior).toConstantValue({ entireEdge: true, entireElement: true });
Expand Down
23 changes: 2 additions & 21 deletions packages/client/src/base/default.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,10 @@
* SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
********************************************************************************/
import '@vscode/codicons/dist/codicon.css';
import { Container } from 'inversify';
import {
ActionHandlerRegistry,
FeatureModule,
InitializeResult,
KeyTool,
LocationPostprocessor,
ModelSource,
MouseTool,
MoveCommand,
SetDirtyStateAction,
Expand All @@ -43,6 +39,7 @@ 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 { GLSPModelRegistry } from './model/model-registry';
import { SelectionClearingMouseListener } from './selection-clearing-mouse-listener';
Expand Down Expand Up @@ -97,6 +94,7 @@ export const defaultModule = new FeatureModule((bind, unbind, isBound, rebind, .
bindOrRebind(context, TYPES.IActionDispatcher).toService(GLSPActionDispatcher);

bindAsService(context, TYPES.ModelSource, GLSPModelSource);
bind(DiagramLoader).toSelf().inSingletonScope();
bind(ModelInitializationConstraint).to(DefaultModelInitializationConstraint).inSingletonScope();

// support re-registration of model elements and views
Expand All @@ -115,20 +113,3 @@ export const defaultModule = new FeatureModule((bind, unbind, isBound, rebind, .
bindAsService(context, TYPES.IVNodePostprocessor, LocationPostprocessor);
bind(TYPES.HiddenVNodePostprocessor).toService(LocationPostprocessor);
});

/**
* Utility function to configure the {@link ModelSource}, i.e. the `DiagramServer`, as action handler for all server actions for the given
* diagramType.
* @param result A promise that resolves after all server actions have been registered.
* @param diagramType The diagram type.
* @param container The di container.
*/
export async function configureServerActions(result: InitializeResult, diagramType: string, container: Container): Promise<void> {
const modelSource = container.get<ModelSource>(TYPES.ModelSource);
const actionHandlerRegistry = container.get<ActionHandlerRegistry>(ActionHandlerRegistry);
const serverActions = result.serverActions[diagramType];
if (serverActions.length === 0) {
throw new Error(`No server-handled actions could be derived from the initialize result for diagramType: ${diagramType}!`);
}
serverActions.forEach(actionKind => actionHandlerRegistry.register(actionKind, modelSource));
}
160 changes: 160 additions & 0 deletions packages/client/src/base/model/diagram-loader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
/********************************************************************************
* 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 { inject, injectable, multiInject, optional, postConstruct } from 'inversify';
import {
ApplicationIdProvider,
Args,
EMPTY_ROOT,
EndProgressAction,
GLSPClient,
MaybePromise,
RequestModelAction,
ServerStatusAction,
SetModelAction,
StartProgressAction,
TYPES
} from '~glsp-sprotty';
import { GLSPActionDispatcher } from '../action-dispatcher';
import { Ranked } from '../ranked';

/**
* Instance specific configuration options for a GLSP diagram
*/
export interface IDiagramOptions {
/**
* Unique id associated with this diagram. Used on the server side to identify the
* corresponding client session.
*/
clientId: string;
/**
* The diagram type i.e. diagram language this diagram is associated with.
*/
diagramType: string;
/**
* The GLSP client used by this diagram to communicate with the server.
*/
glspClient: GLSPClient;
/**
* The file source URI associated with this diagram.
*/
sourceUri?: string;
}

/**
* Services that implement startup hooks which are invoked during the {@link DiagramLoader.load} process.
* Typically used to dispatch additional initial actions and/or activate UI extensions on startup.
* Execution order is derived by the `rank` property of the service. If not present, the {@link Ranked.DEFAULT_RANK} will be assumed.
*
*/
export interface IDiagramStartup extends Partial<Ranked> {
/**
* Hook for services that should be executed before the underlying GLSP client is configured and the server is initialized.
*/
preInitialize?(): MaybePromise<void>;
/**
* Hook for services that should be executed before the initial model loading request (i.e. `RequestModelAction`) but
* after the underlying GLSP client has been configured and the server is initialized.
*/
preModelLoading?(): MaybePromise<void>;
/**
* 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.
*/
postModelLoading?(): MaybePromise<void>;
}

export namespace IDiagramStartup {
export function is(object: unknown): object is IDiagramStartup {
return Ranked.is(object) && ('preInitialize' in object || 'preModelLoading' in object || 'postModelLoading' in object);
}
}

/**
* The central component responsible for initializing the diagram and loading the graphical model
* from the GLSP server.
* Invoking the {@link DiagramLoader.load} method is typically the first operation that is executed after
* a diagram DI container has been created
*/
@injectable()
export class DiagramLoader {
@inject(TYPES.IDiagramOptions)
protected options: IDiagramOptions;

@inject(GLSPActionDispatcher)
protected actionDispatcher: GLSPActionDispatcher;

@multiInject(TYPES.IDiagramStartup)
@optional()
protected diagramStartups: IDiagramStartup[] = [];

protected enableLoadingNotifications = true;

@postConstruct()
protected postConstruct(): void {
this.diagramStartups.sort((a, b) => Ranked.getRank(a) - Ranked.getRank(b));
}

async load(requestModelOptions: Args = {}): Promise<void> {
// Set placeholder model until real model from server is available
await this.actionDispatcher.dispatch(SetModelAction.create(EMPTY_ROOT));
await this.invokeStartupHook('preInitialize');
await this.configureGLSPClient();
await this.invokeStartupHook('preModelLoading');
await this.requestModel(requestModelOptions);
await this.invokeStartupHook('postModelLoading');
}

protected async invokeStartupHook(hook: keyof Omit<IDiagramStartup, 'rank'>): Promise<void> {
for (const startup of this.diagramStartups) {
await startup[hook]?.();
}
}

protected requestModel(requestModelOptions: Args = {}): Promise<void> {
const options = { sourceUri: this.options.sourceUri, diagramType: this.options.diagramType, ...requestModelOptions } as Args;
return this.actionDispatcher.dispatch(RequestModelAction.create({ options }));
}

protected async configureGLSPClient(): Promise<void> {
const glspClient = this.options.glspClient;

if (this.enableLoadingNotifications) {
this.actionDispatcher.dispatchAll([
ServerStatusAction.create('Initializing...', { severity: 'INFO' }),
StartProgressAction.create({ progressId: 'initializeClient', title: 'Initializing' })
]);
}

await glspClient.start();

if (!glspClient.initializeResult) {
await glspClient.initializeServer({
applicationId: ApplicationIdProvider.get(),
protocolVersion: GLSPClient.protocolVersion
});
}

if (this.enableLoadingNotifications) {
this.actionDispatcher.dispatchAll([
ServerStatusAction.create('', { severity: 'NONE' }),
EndProgressAction.create('initializeClient')
]);
}
}
}
Loading

0 comments on commit 0be9853

Please sign in to comment.