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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@

- [core] added `errorHandling` option to `Emitter` to control how listener exceptions are handled: `'log'` (default), `'propagate'` (collect and re-throw), or a custom callback [#17332](https://github.com/eclipse-theia/theia/pull/17332)
- [editor] replaced the per-URI editor counter system in `EditorManager` with random counters [#17275](https://github.com/eclipse-theia/theia/pull/17275)
- [core] the Inversify containers created in the backend for each frontend connection (connection-scoped child containers) now unbind all services via `Container::unbindAllAsync()` upon closure of the RPC channel. This allows clean-up methods annotated with `@preDestroy()` to run, releasing resources. Downstream applications should be aware that service instances deliberately shared between connection-scoped containers, and service instances obtained from the root container that are explicitly bound again in the connection-scoped module, will cause such shared services to be destroyed while still in use in other containers. Both of these scenarios are already Inversify anti-patterns, so are not expected to arise in practice. [#17384](https://github.com/eclipse-theia/theia/pull/17384)

<a name="breaking_changes_1.71.0">[Breaking Changes:](#breaking_changes_1.71.0)</a>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ export class DefaultMessagingService implements MessagingService, BackendApplica
protected createMainChannelContainer(socket: Channel): Container {
const connectionContainer: Container = this.container.createChild() as Container;
connectionContainer.bind(MainChannel).toConstantValue(socket);
socket.onClose(() => connectionContainer.unbindAllAsync().catch(e => console.error(e)));
return connectionContainer;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// *****************************************************************************
// Copyright (C) 2026 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-only WITH Classpath-exception-2.0
// *****************************************************************************

import { expect } from 'chai';
import { Container, injectable, preDestroy } from 'inversify';
import { ConnectionHandler, bindContributionProvider, servicesPath } from '../../../common';
import { BasicChannel, Channel } from '../../../common/message-rpc/channel';
import { Uint8ArrayWriteBuffer } from '../../../common/message-rpc/uint8-array-message-buffer';
import { ConnectionContainerModule } from '../connection-container-module';
import { DefaultMessagingService, MessagingContainer } from '../default-messaging-service';
import { FrontendConnectionService } from '../frontend-connection-service';
import { MessagingService } from '../messaging-service';

describe('DefaultMessagingService', () => {

describe('when a frontend connection closes', () => {

it('disposes the connection-scoped child container, invoking @preDestroy on bound singleton services', async () => {
let canaryDisposed = false;

@injectable()
class CanaryConnectionHandler implements ConnectionHandler {
readonly path = 'canary';
onConnection(_channel: Channel): void { /* not relevant for this test */ }

@preDestroy()
protected onPreDestroy(): void {
canaryDisposed = true;
}
}

const canaryModule = ConnectionContainerModule.create(({ bind }) => {
bind(CanaryConnectionHandler).toSelf().inSingletonScope();
bind(ConnectionHandler).toService(CanaryConnectionHandler);
});

const container = new Container();
container.bind(MessagingContainer).toConstantValue(container);
container.bind(DefaultMessagingService).toSelf().inSingletonScope();
container.bind(ConnectionContainerModule).toConstantValue(canaryModule);
bindContributionProvider(container, ConnectionContainerModule);
bindContributionProvider(container, MessagingService.Contribution);

let serviceHandler: ((params: MessagingService.PathParams, mainChannel: Channel) => void) | undefined;
const frontendConnectionService: FrontendConnectionService = {
registerConnectionHandler(path, callback): void {
if (path === servicesPath) {
serviceHandler = callback;
}
}
};
container.bind(FrontendConnectionService).toConstantValue(frontendConnectionService);

const messagingService = container.get(DefaultMessagingService);
messagingService.initialize();

expect(serviceHandler, 'connection handler not registered on the services path').to.not.be.undefined;

const mainChannel = new BasicChannel(() => new Uint8ArrayWriteBuffer());
serviceHandler!({}, mainChannel);

expect(canaryDisposed, 'canary should not be disposed before the channel is closed').to.be.false;

mainChannel.onCloseEmitter.fire({ reason: 'frontend connection closed' });
await new Promise<void>(resolve => setImmediate(resolve));

expect(canaryDisposed, 'canary @preDestroy was not invoked').to.be.true;
});

});

});
Loading