Skip to content
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
12 changes: 12 additions & 0 deletions docs/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"overrides": [
{
"files": "*.md",
"options": {
"tabWidth": 2,
"singleQuote": true,
"printWidth": 160
}
}
]
}
2 changes: 1 addition & 1 deletion examples/theia/electron-app/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
},
"devDependencies": {
"@theia/cli": "*",
"electron": "^23.3.13",
"electron": "30.1.2",
"rimraf": "^6.0.1"
},
"scripts": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,12 @@ export abstract class ModelFormWidget<M extends NonNullable<object>>
@inject(ILogger)
protected readonly logger: ILogger;

protected readonly onContentChangeEmitter = new Emitter<void>();

get onContentChanged() {
return this.onContentChangeEmitter.event;
}

private _dirty = false;
private dirtyChangedEmitter = new Emitter<void>();
readonly onDirtyChanged = this.dirtyChangedEmitter.event;
Expand Down
4 changes: 2 additions & 2 deletions ext/model-service-theia/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
},
"engines": {
"yarn": ">=1.7.0 <2.x.x",
"node": ">=18.0.0"
"node": ">=20.14.0"
},
"files": [
"lib",
Expand All @@ -45,7 +45,7 @@
"@types/chai": "^4.2.22",
"@types/chai-like": "^1.1.1",
"@types/mocha": "^9.0.0",
"@types/node": "^16.10.3",
"@types/node": "^20.14.0",
"@types/sinon": "^10.0.13",
"@types/sinon-chai": "^3.2.9",
"@typescript-eslint/eslint-plugin": "^6.7.5",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const MODEL1 = { name: 'Model 1' };
describe('FrontendModelHubProvider', () => {
const appContext = 'test-app';

let fake: FakeModelHubProtocol;
let fake: FakeModelHubProtocol<string>;
let subscriber: FrontendModelHubSubscriber;
let modelHubProvider: FrontendModelHubProvider<string>;
let bombFrontendModelHubImpl: () => void;
Expand All @@ -62,7 +62,7 @@ describe('FrontendModelHubProvider', () => {
FrontendModelHubProvider
);

fake = container.get(FakeModelHubProtocol);
fake = container.get<FakeModelHubProtocol<string>>(FakeModelHubProtocol);
fake.setModel(MODEL1_ID, MODEL1);
connectClient(fake, subscriber);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ describe('FrontendModelHubSubscriber', () => {
const appContext = 'test-app';

let sandbox: sinon.SinonSandbox;
let fake: FakeModelHubProtocol;
let fake: FakeModelHubProtocol<string>;
let subscriber: FrontendModelHubSubscriber;
let tracker: ModelHubTracker;

Expand All @@ -78,7 +78,7 @@ describe('FrontendModelHubSubscriber', () => {
);
tracker = container.get<ModelHubTracker>(ModelHubTracker);

fake = container.get(FakeModelHubProtocol);
fake = container.get<FakeModelHubProtocol<string>>(FakeModelHubProtocol);
fake.setModel(MODEL1_ID, MODEL1);
connectClient(fake, subscriber);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ describe('FrontendModelHub', () => {

let sandbox: sinon.SinonSandbox;
let modelHub: FrontendModelHub;
let fake: FakeModelHubProtocol;
let fake: FakeModelHubProtocol<string>;
let provider: FrontendModelHubProvider;

beforeEach(async () => {
Expand All @@ -73,7 +73,7 @@ describe('FrontendModelHub', () => {
FrontendModelHubSubscriber
);

fake = container.get(FakeModelHubProtocol);
fake = container.get<FakeModelHubProtocol<string>>(FakeModelHubProtocol);
fake.setModel(MODEL1_ID, MODEL1);
connectClient(fake, subscriber);
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ export function bindFrontendModelHubProvider(bind: interfaces.Bind): void {
sub.onModelHubCreated = (createdContext) => {
if (createdContext === context) {
try {
resolve(child.get(FrontendModelHubImpl));
resolve(
child.get<FrontendModelHubImpl<string>>(FrontendModelHubImpl)
);
} catch (error) {
reject(error);
}
Expand Down
6 changes: 6 additions & 0 deletions ext/model-service-theia/src/browser/frontend-model-hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
import {
ModelHub,
ModelHubSubscription,
ModelServiceSubscription,
} from '@eclipse-emfcloud/model-service';
import { Diagnostic } from '@eclipse-emfcloud/model-validation';
import { ILogger } from '@theia/core';
Expand Down Expand Up @@ -80,11 +81,16 @@ export type FrontendModelHub<K = string> = {
| 'getModelService'
| 'liveValidation'
| 'getModelAccessorBus'
| 'subscribe'
>]: MakeAsync<ModelHub<K, string>[key]>;
} & {
readonly context: string;
readonly isDisposed: boolean;
getModelAccessorBus: () => FrontendModelAccessorBus;
subscribe<M extends object = object>(): Promise<ModelHubSubscription<K, M>>;
subscribe<M extends object = object>(
...modelIds: K[]
): Promise<ModelServiceSubscription<K, M>>;
};

export const FrontendModelHubContext = Symbol('FrontendModelHubContext');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import {
RpcConnectionFactory,
bindFakeRpcConnectionFactory,
} from './fake-json-rpc';
import { Emitter, Event } from '@theia/core';

type FakeModel = Record<string, unknown>;
const FAKE_MODEL_ID = 'test.fake-model';
Expand All @@ -58,26 +59,44 @@ function createTestContainer(contrib: ModelServiceContribution): Container {
type FullModelAccessorBusServer = ModelAccessorBusServer<string> & {
getModelAccessorBus: (context: string) => Promise<ModelAccessorBus>;
};

describe('Model Accessor Bus Server', () => {
const appContext = 'test-app';

let sandbox: sinon.SinonSandbox;
let modelAccessorBusServer: ModelAccessorBusServer;
let modelAccessorBusServer2: ModelAccessorBusServer;

let fakeContribution: FakeContribution;
let clientProxy: {
[key in keyof ModelAccessorBusClient]: sinon.SinonSpy<
Parameters<ModelAccessorBusClient[key]>
>;
onAccessorChanged: sinon.SinonSpy;
closeSubscription: sinon.SinonSpy;
onDidOpenConnection: Event<void>;
onDidCloseConnection: Event<void>;
closeConnection: () => void;
};
let clientProxy2: {
onAccessorChanged: sinon.SinonSpy;
closeSubscription: sinon.SinonSpy;
onDidOpenConnection: Event<void>;
onDidCloseConnection: Event<void>;
closeConnection: () => void;
};

beforeEach(async () => {
sandbox = sinon.createSandbox();
fakeContribution = new FakeContribution();
const container = createTestContainer(fakeContribution);
const factory = container.get(RpcConnectionFactory);
const closeConnectionEmitter = new Emitter<void>();
clientProxy = {
onAccessorChanged: sandbox.stub(),
closeSubscription: sandbox.stub(),
onDidOpenConnection: new Emitter<void>().event,
onDidCloseConnection: closeConnectionEmitter.event,
closeConnection(): void {
closeConnectionEmitter.fire();
},
};
modelAccessorBusServer = await factory.getServer<ModelAccessorBusServer>(
ModelAccessorBusProtocolServicePath,
Expand Down Expand Up @@ -275,6 +294,7 @@ describe('Model Accessor Bus Server', () => {
expect(clientProxy.onAccessorChanged.calledOnceWith(1)).to.be.true;
});
});

describe('getModelAccessorBus', async () => {
it('expect to be different on different contexts', async () => {
const spiedAccessorBusAccess = sandbox.spy(
Expand Down Expand Up @@ -307,6 +327,45 @@ describe('Model Accessor Bus Server', () => {
expect(bus_0 === bus_1).to.be.true;
});
});

describe('multiple clients', () => {
beforeEach(async () => {
const closeConnectionEmitter = new Emitter<void>();
clientProxy2 = {
onAccessorChanged: sandbox.stub(),
closeSubscription: sandbox.stub(),
onDidOpenConnection: new Emitter<void>().event,
onDidCloseConnection: closeConnectionEmitter.event,
closeConnection(): void {
closeConnectionEmitter.fire();
},
};
const container = createTestContainer(fakeContribution);
const factory = container.get(RpcConnectionFactory);
modelAccessorBusServer2 = await factory.getServer<ModelAccessorBusServer>(
ModelAccessorBusProtocolServicePath,
clientProxy2
);
});

it('supports multiple clients', async () => {
expect(modelAccessorBusServer2).to.exist;
expect(modelAccessorBusServer2).not.to.be.equal(modelAccessorBusServer); // Different instances
});

it('can close client', async () => {
const token = await modelAccessorBusServer.subscribe(appContext, 'test');
clientProxy.closeConnection();
// closeSubscription should be called before the client is removed. A real
// client would not receive the event (since the connection is closed), but our
// mock client doesn't actually have a connection to close.
sinon.assert.calledWithExactly(clientProxy.closeSubscription, token.id);
expect(modelAccessorBusServer.getClient()).to.be.undefined;

// Other server+client still exists
expect(modelAccessorBusServer2.getClient()).to.exist;
});
});
});

export class MockProvider extends HubAwareProvider {
Expand Down
92 changes: 75 additions & 17 deletions ext/model-service-theia/src/node/__tests__/model-hub-server.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
RpcConnectionFactory,
bindFakeRpcConnectionFactory,
} from './fake-json-rpc';
import { Emitter, RpcProxy } from '@theia/core';

chai.use(chaiLike);
chai.use(sinonChai);
Expand All @@ -65,35 +66,58 @@ function createTestContainer(
return container;
}

type ClientProxy = RpcProxy<ModelHubClient> & {
[key in keyof ModelHubClient]: sinon.SinonSpy<
Parameters<ModelHubClient[key]>
>;
};

class MockClientProxy implements ClientProxy {
// ModelHub Client spies

connectionListeners: (() => void)[] = [];
onModelChanged = sinon.stub<Parameters<ClientProxy['onModelChanged']>>();
onModelDirtyState =
sinon.stub<Parameters<ClientProxy['onModelDirtyState']>>();
onModelValidated = sinon.stub<Parameters<ClientProxy['onModelValidated']>>();
onModelLoaded = sinon.stub<Parameters<ClientProxy['onModelLoaded']>>();
onModelUnloaded = sinon.stub<Parameters<ClientProxy['onModelUnloaded']>>();
onModelHubDisposed =
sinon.stub<Parameters<ClientProxy['onModelHubDisposed']>>();
closeSubscription =
sinon.stub<Parameters<ClientProxy['closeSubscription']>>();
onModelHubCreated =
sinon.stub<Parameters<ClientProxy['onModelHubCreated']>>();
onModelHubDestroyed =
sinon.stub<Parameters<ClientProxy['onModelHubDestroyed']>>();

// Client lifecycle events

private openConnectionEmitter = new Emitter<void>();
private closeConnectionEmitter = new Emitter<void>();
onDidOpenConnection = this.openConnectionEmitter.event;
onDidCloseConnection = this.closeConnectionEmitter.event;
closeConnection(): void {
this.closeConnectionEmitter.fire();
}
}

describe('ModelHubServer', () => {
const appContext = 'test-app';

let sandbox: sinon.SinonSandbox;
let contrib1: Contribution1;
let modelHubServer: ModelHubServer;
let clientProxy: {
[key in keyof ModelHubClient]: sinon.SinonSpy<
Parameters<ModelHubClient[key]>
>;
};
let clientProxy: MockClientProxy;
let modelHub: ModelHub;
let container: Container;

beforeEach(async () => {
sandbox = sinon.createSandbox();
contrib1 = new Contribution1();
const container = createTestContainer(contrib1);
container = createTestContainer(contrib1);
const factory = container.get(RpcConnectionFactory);
clientProxy = {
onModelChanged: sandbox.stub(),
onModelDirtyState: sandbox.stub(),
onModelValidated: sandbox.stub(),
onModelLoaded: sandbox.stub(),
onModelUnloaded: sandbox.stub(),
onModelHubDisposed: sandbox.stub(),
closeSubscription: sandbox.stub(),
onModelHubCreated: sandbox.stub(),
onModelHubDestroyed: sandbox.stub(),
};
clientProxy = new MockClientProxy();
modelHubServer = await factory.getServer<ModelHubServer>(
ModelHubProtocolServicePath,
clientProxy
Expand Down Expand Up @@ -346,6 +370,40 @@ describe('ModelHubServer', () => {
);
});
});

describe('multiple clients', () => {
let clientProxy2: MockClientProxy;
let modelHubServer2: ModelHubServer;

beforeEach(async () => {
clientProxy2 = new MockClientProxy();
const factory = container.get(RpcConnectionFactory);
modelHubServer2 = await factory.getServer<ModelHubServer>(
ModelHubProtocolServicePath,
clientProxy2
);
});

it('supports multiple clients', async () => {
expect(modelHubServer2).to.exist;
expect(modelHubServer2).not.to.be.equal(modelHubServer); // Different instances
});

it('can close client', async () => {
const token = await modelHubServer.subscribe(appContext, MODEL1_ID);
clientProxy.closeConnection();
// closeSubscription should be called before the client is removed. A real
// client would not receive the event (since the connection is closed), but our
// mock client doesn't actually have a connection to close.
sinon.assert.calledWithExactly(clientProxy.closeSubscription, token.id);

// Other server+client still works as expected
await modelHubServer2.getModel(appContext, MODEL1_ID);
expect(clientProxy2.onModelHubCreated).to.have.been.calledWith(
appContext
);
});
});
});

interface ModelService1 {
Expand Down
Loading