-
Notifications
You must be signed in to change notification settings - Fork 2.5k
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
refactor core messaging and RPC (electron-only so far) #11076
Conversation
64f6d3f
to
0ab5141
Compare
e6c6472
to
4376020
Compare
Hey @paul-marechal I had a glance at the PR today, and there are a couple of things which make it hard to review right now:
I really want to understand where you're going with this change. Maybe we can do another live session and clarify the gist of it? |
Hi @paul-marechal, If I understood correctly this change currently only refactors the Theia extension messaging API. I wonder whether it's feasible to also apply similar changes to the message RPC protocol of the plugin API. There we don't have access to inversify. Could this be a potential problem? In general, I definitely like the object-based connection approach more than the binary channel approach we introduced in #11011. |
export const electronMainWindowServicePath = servicePath<ElectronMainWindowService>('/services/electron-window'); | ||
export const ElectronMainWindowService = serviceIdentifier<ElectronMainWindowService>('ElectronMainWindowService'); | ||
export interface ElectronMainWindowService { | ||
openNewWindow(url: string, options?: NewWindowOptions): undefined; | ||
openNewDefaultWindow(): void; | ||
openNewWindow(url: string, options?: NewWindowOptions): MaybePromise<void>; | ||
openNewDefaultWindow(): MaybePromise<void>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another seemingly drive-by change that allowed me to actually type check the bindings for it.
Without this TypeScript wasn't giving me any type checking...
/** | ||
* Part of Theia's Service Layer. | ||
* | ||
* Requested services to offer over RPC are fetched through a `ServiceProvider` that will source from `ServiceContribution` bindings. | ||
* | ||
* ## Usage Examples | ||
* | ||
* ### Record | ||
* | ||
* ```ts | ||
* bind(ServiceContribution) | ||
* .toDynamicValue(ctx => ({ | ||
* [PATH1]: () => ctx.container.get(Service1), | ||
* [PATH2]: () => ctx.container.get(Service2), | ||
* [PATH3]: params => ctx.container.get(params.yourParam ? Service3 : Service4); | ||
* // ... | ||
* })) | ||
* .inSingletonScope() | ||
* .whenTargetNamed(YourServiceNamespace); | ||
* ``` | ||
* | ||
* ### Function | ||
* | ||
* ```ts | ||
* bind(ServiceContribution) | ||
* .toDynamicValue(ctx => (serviceId, params) => { | ||
* // process arguments... | ||
* return resolvedServiceOrUndefined; | ||
* })) | ||
* .inSingletonScope() | ||
* .whenTargetNamed(YourServiceNamespace); | ||
* ``` | ||
*/ | ||
export const ServiceContribution = serviceIdentifier<ServiceContribution>('ServiceContribution'); | ||
export type ServiceContribution = ServiceContributionFunction | ServiceContributionRecord; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export type ServiceContributionFunction = (serviceId: string, params?: any) => any; | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export interface ServiceContributionRecord { [serviceId: string]: (params?: any) => any }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Service contributions are one of the new layers: When you want to do RPC rather than explicitly binding services to work over one protocol or the other, you can use this contribution point to provide the instance to expose via RPC, and connection handlers may choose any RPC implementation that will fetch the instance to serve from the contributed services.
e.g. Frontend requests for a proxy at /jsonrpc/some/service/path
via socket.io. Socket.io receives the new connection and routes it to the /jsonrpc/*serviceId
connection handler which runs JSON-RPC over the incoming connection. This JSON-RPC connection handler then looks up the service for serviceId
through ServiceContribution
bindings, if it find an instance it will wire it to answer JSON-RPC requests over the incoming connection. Done.
export default new ContainerModule(bind => { | ||
// #region transients | ||
bind(DefaultConnectionMultiplexer).toSelf().inTransientScope(); | ||
bind(DefaultRpcProxyProvider).toSelf().inTransientScope(); | ||
bind(ContainerScopeRegistry).to(DefaultContainerScopeRegistry).inTransientScope(); | ||
// #endregion | ||
// #region singletons | ||
bind(RpcProxying).to(DefaultRpcProxying).inSingletonScope(); | ||
bind(Reflection).to(DefaultReflection).inSingletonScope(); | ||
// #endregion | ||
// #region factories | ||
bind(ConnectionTransformer) | ||
.toFunction((connection, transformer) => new TransformedConnection(connection, transformer)); | ||
bind(DeferredConnectionFactory) | ||
.toFunction(promise => new DeferredConnection(promise)); | ||
bind(JsonRpcConnectionFactory) | ||
.toFunction(connection => new JsonRpcConnection(connection)); | ||
bind(ContainerScopeFactory) | ||
.toFunction((container, callbacks) => new DefaultContainerScope(container, callbacks)); | ||
bind(RcFactory) | ||
.toFunction(ref => DefaultRc.New(ref)); | ||
bind(LazyProxyFactory) | ||
.toDynamicValue(ctx => promise => { | ||
// eslint-disable-next-line no-null/no-null | ||
const nullObject = Object.freeze(Object.create(null)); | ||
const reflection = ctx.container.get(Reflection); | ||
const proxyHandler = new LazyProxyHandler(promise, reflection); | ||
return new Proxy(nullObject, proxyHandler); | ||
}) | ||
.inSingletonScope(); | ||
// #endregion | ||
}); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This container module just contains new re-usable components to facilitate wiring using the new APIs.
// We need to setup the JSON-RPC connection between electron-main and backend: | ||
// We'll multiplex messages over a Node IPC connection and talk JSON-RPC over the channels. | ||
bind(ConnectionMultiplexer) | ||
.toDynamicValue(ctx => { | ||
const transformer = ctx.container.get(ConnectionTransformer); | ||
const nodeIpcConnectionFactory = ctx.container.get(NodeIpcConnectionFactory); | ||
const deferredConnectionFactory = ctx.container.get(DeferredConnectionFactory); | ||
const parentIpc = nodeIpcConnectionFactory(process); | ||
const sharedIpc: AnyConnection = transformer(parentIpc, { | ||
decode: (message, emit) => { | ||
if (typeof message === 'object' && THEIA_ELECTRON_IPC_CHANNEL_NAME in message) { | ||
emit(message[THEIA_ELECTRON_IPC_CHANNEL_NAME]); | ||
} | ||
}, | ||
encode: (message, write) => { | ||
write({ [THEIA_ELECTRON_IPC_CHANNEL_NAME]: message }); | ||
} | ||
}); | ||
const deferredConnection = deferredConnectionFactory(waitForRemote(sharedIpc)); | ||
return ctx.container.get(DefaultConnectionMultiplexer).initialize(deferredConnection); | ||
}) | ||
.inSingletonScope() | ||
.whenTargetNamed(ElectronMainAndBackend); | ||
bind(BackendApplicationContribution) | ||
.toDynamicValue(ctx => ({ | ||
initialize(): void { | ||
const multiplexer = ctx.container.getNamed(ConnectionMultiplexer, ElectronMainAndBackend); | ||
const serviceProvider = ctx.container.getNamed(ServiceProvider, ElectronMainAndBackend); | ||
const jsonRpcConnectionFactory = ctx.container.get(JsonRpcConnectionFactory); | ||
const rpcProxying = ctx.container.get(RpcProxying); | ||
multiplexer.listen(({ serviceId, serviceParams }, accept, next) => { | ||
const service = serviceProvider.getService(serviceId, serviceParams); | ||
if (service) { | ||
rpcProxying.serve(service, jsonRpcConnectionFactory(accept())); | ||
} else { | ||
next(); | ||
} | ||
}); | ||
} | ||
})) | ||
.inSingletonScope(); | ||
bind(ProxyProvider) | ||
.toDynamicValue(ctx => { | ||
const multiplexer = ctx.container.getNamed(ConnectionMultiplexer, ElectronMainAndBackend); | ||
const jsonRpcConnectionFactory = ctx.container.get(JsonRpcConnectionFactory); | ||
return ctx.container.get(DefaultRpcProxyProvider).initialize( | ||
(serviceId, serviceParams) => jsonRpcConnectionFactory(multiplexer.open({ serviceId, serviceParams })) | ||
); | ||
}) | ||
.inSingletonScope() | ||
.whenTargetNamed(ElectronMainAndBackend); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an example of what I meant by "wiring might look more complex" with the new API:
The most important binding here is the ProxyProvider
named ElectronMainAndBackend
. The idea is that each runtime may need proxies from multiple other runtimes, so we need a way to identify which is which and this is now done with named bindings.
This allows developers to write:
bind(MyRemoteService)
.toDynamicValue(ctx => ctx.container.getNamed(ProxyProvider, Target).getProxy(remoteServicePath))
.inSingletonScope();
The other bindings are only there as implementation details:
We need a re-usable ConnectionMultiplexer
to create remote channels in order to create our proxies as well as listen for incoming channels to serve the other side's requests for their own proxies (bidirectional).
The BackendApplicationContribution
is there to force the creation of the ConnectionMultiplexer
and start listening straight away. The reason is that if we left it to the ProxyProvider
to resolve the listening bindings, then if an application never makes use of proxies then ProxyProvider
will never be resolved, nor ConnectionMultiplexer
. We need to explicitly setup the listening bits.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe I'm not understanding the concepts here yet, but it just seem I need to understand too many interlocking concepts at once here: If I think about what we're trying to achieve, it's not that complicated:
- The system establishes a number of well know point-to-point connections at startup, for example front end <=> back end or front end <=> plugin host or back end <=> file system change watcher
- Some clients want to bind service objects whose lifecycle it bound to those connections when the connections are established.
- Some clients bind service proxies to those connections.
When we establish a connection, we establish a service scope for those services. The connection being established is put inside this scope and allow any listeners to react to the connection.
We can bind a rpc protocol factory that will create an rpc protocol instance in this new scope. it will be bound to the connection.
We need a contribution point that allows clients to bind service object factories and client proxy factories inside this scope. There needs to be an initialization protocol between clients and services that is two-phase: only after full initialization are remote calls allowed.
The services are bound to a named endpoints, for example: "BackEndFrontEnd" or "PluginHost". Note that multiple instances of the same endpoint may be established (such as with plugin hosts).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well the above code is doing us a disservice as there doesn't need to be connection scopes in this case: There is a 1-to-1 relationship between the electron-main runtime and the backend runtime.
Connections scopes are only required when you have a 1-to-many relationship like between backend and frontends.
In such a case, not available in this PR yet, I have wired an internal router that takes connections targeted at the backend and tries to identify the origin frontend instance to properly route it to the appropriate scoped Inversify container. Connection handlers are then run in that scoped container, like we do now. The handlers will determine if we're doing RPC or anything else over the new connection.
@injectable() | ||
export class SampleUpdaterClientImpl implements SampleUpdaterClient { | ||
|
||
protected readonly onReadyToInstallEmitter = new Emitter<void>(); | ||
readonly onReadyToInstall = this.onReadyToInstallEmitter.event; | ||
|
||
notifyReadyToInstall(): void { | ||
this.onReadyToInstallEmitter.fire(); | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a very important change with the new RPC/proxy API: we no longer setup "clients" to get notifications back from the remote: we can use Event
fields directly instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure this is actually an improvement. How is this better than the old way of setting up service objects on both ends?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I fear this might make initialization of service objects more complicated.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't see how this makes things more complicated. I keep deleting code to use this new API.
@inject(SampleUpdaterClientImpl) | ||
protected readonly updaterClient: SampleUpdaterClientImpl; | ||
|
||
protected readyToUpdate = false; | ||
|
||
@postConstruct() | ||
protected init(): void { | ||
this.updaterClient.onReadyToInstall(async () => { | ||
this.updater.onReadyToInstall(async () => { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here's an example of directly using Event
fields directly on the proxy. Less boilerplate code than with clients.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I generally don't mind boilerplate code if it's simpler to understand than the alternative. Typing is not the problem in software construction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Typing prevents mistakes. This apart, the previous required boilerplate code for the xxxClient
API was often misused which I assumed was a symptom of a design problem.
The new API allows servers to expose their events through proxies. For requests going the other way from the main RPC setup you can just create a new dedicated proxy.
Just as an example, the most egregious implementation I've seen using clients must be this one:
theia/packages/terminal/src/browser/terminal-frontend-contribution.ts
Lines 199 to 206 in fd14c0b
this.terminalWatcher.onUpdateTerminalEnvVariablesRequested(() => { | |
this.storageService.getData<string>(ENVIRONMENT_VARIABLE_COLLECTIONS_KEY).then(data => { | |
if (data) { | |
const collectionsJson: SerializableExtensionEnvironmentVariableCollection[] = JSON.parse(data); | |
collectionsJson.forEach(c => this.shellTerminalServer.setCollection(c.extensionIdentifier, true, c.collection)); | |
} | |
}); | |
}); |
Here TerminalWatcher
is a client to TerminalServer
. And instead of having the remote server call a method to fetch collections from its clients and use the result, it does this roundabout procedure where it sends an event and expects the receiving side to initiate subsequent RPC calls (plural) with this.shellTerminalServer.setCollection(...)
.
In my latest changes I replaced the idea of the client on the server side by a proxy from the frontend implementing storage methods. The server just does environmentStore.getEnvironmentVariables()
or environmentStore.saveEnvironmentVariables(vars)
, it is transparently handled by the frontend.
return new Proxy(target, new DisposableProxyHandler()); | ||
} | ||
|
||
export class DisposableProxyHandler<T extends Disposable> implements ProxyHandler<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This file is not used anymore, but it gives a tool to wrap disposables so that we get warnings (without throwing) whenever we use a disposed instance because that's a bug.
/** | ||
* Represents a scoped connection to a remote service on which to call methods. | ||
* | ||
* There should be a 1-to-1 relationship between a `RpcConnection` and the | ||
* remote service it represents. | ||
*/ | ||
export const RpcConnection = serviceIdentifier<RpcConnection>('RpcConnection'); | ||
export interface RpcConnection { | ||
onClose: Event<void> | ||
onRequest(handler: (method: string, params: any[], token: CancellationToken) => any): void | ||
onNotification(handler: (method: string, params: any[]) => void): void | ||
sendRequest<T>(method: string, params: any[]): Promise<T> | ||
sendNotification(method: string, params: any[]): void | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In my opinion this is one of the most important new abstractions in this change.
No matter how your RPC protocol works, or how you establish the connection, as long as you respect this interface we can create and serve JavaScript proxies.
Instances that implement this interface should be scoped to a service.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
IMO, we need to think about what we're abstracting here: is it transport (socket, electron, ipc) or is it lifecycle? I think the concepts adopters should deal with are service objects (proxies and servers) and transports (or channels in the related PR). Everything in between should be an implementation detail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is an implementation detail, but remote procedure calls inherently carry the idea that there's some underlying "connection" to a remote. The connection related event onClose
is only there to propagate to users of this API that the link is no longer valid and further calls won't work anymore. If the link never breaks, onClose
will never be fired. Note that this API doesn't leak more than that to users, only notify proxies that RPC won't work anymore with the current instance.
/** | ||
* A `Connection` allows you to listen for messages and send messages back. | ||
* | ||
* Messages must arrive integrally and in the order they are sent! | ||
* | ||
* Most implementations are going to be API adapters. | ||
*/ | ||
export const Connection = serviceIdentifier<Connection<any>>('Connection'); | ||
export interface Connection<T> { | ||
readonly state: ConnectionState | ||
onOpen: Event<void> | ||
onClose: Event<void> | ||
/** | ||
* Emitted when something goes wrong with the underlying transport. | ||
* Typically when a message fails to be sent or received. | ||
*/ | ||
onError: Event<Error> | ||
onMessage: Event<T> | ||
sendMessage(message: T): void | ||
close(): void | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Second most important abstraction: a normalized interface to send/receive messages.
We handle so many APIs to do communications (sockets, node IPC, electron IPC, custom multiplexed channels, etc) and none really has the same API. We use them all in the same way: to send and receive messages.
This type wouldn't be required if it weren't for the other components I created that are used to help with setting up communication stacks.
e.g. I need to do multiplexing over different IPC APIs for Electron but I only want one multiplexer implementation. By using the Connection
type I can just write adapters for each API and use both in the same DefaultConnectionMultiplexer
implementation.
Another important thing to keep in mind with connections: They may not only send strings or buffers. Most APIs support automatic serialization, and some of those implementations do a much better job than any other JSON.stringify
we would call ourselves.
e.g. Socket.io will optimize the transmission of buffers sent through it. By thinking we should just send strings we called JSON.stringify
on our structures before passing them to Socket.io, preventing it from properly sending our data because we passed it as a raw string.
The idea with connections then is to understand what the underlying transport supports sending and stick to that, unless we have more specific needs. You may see more Connection<any>
than Connection<string>
in this PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We've discussed the necessity to encode/decode messages at some point. Either we assume a transport takes care of object encoding or we assume the "application layer" does.
Either way, we can run a stateful rpc protocol (request/reply) on the abstraction of "Channel" or "Connection" and the protocol should be separate from the transport. IMO, separating encoding/transport/protocol is the right way to go in the long run.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Either we assume a transport takes care of object encoding or we assume the "application layer" does.
Ultimately if you implement a component that relies on some connection, I believe you should assume that the connection will support whatever data objects you need to send. Only when thinking about instantiating this component and plugging it to the rest of the systems should you then consider if serialization is required for your data types or not. If your data needs special encoding, you should find a way to convert it at the wiring level, not so much from the component's design perspective.
Note that I am in the process of replacing the frontend/backend API with the new one. I initially thought it would be better to merge only the Electron bits but I don't think so anymore... I'll mark the PR ready for review once I'm finished with frontend/backend. This should not block #11011. In the meantime the high-level abstractions and ideas won't change, so feedback is still welcome on the new approach! e.g. Named |
I don't have an ETA on the backend-frontend refactoring part, but I believe that I passed the mid-point. Mostly fighting with previous code oddities. |
@@ -91,7 +91,7 @@ const serverModule = require('./server'); | |||
const serverAddress = main.start(serverModule()); | |||
|
|||
serverAddress.then(({ port, address }) => { | |||
if (process && process.send) { | |||
if (process.send) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is what I mean by "drive by fix". Yes, it's a good fix, but unrelated to the problem at hand.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fair enough on this one.
/** | ||
* Use this to create "typed symbols" for Inversify typings to understand our bindings. | ||
*/ | ||
export function serviceIdentifier<T>(name: string): SymbolIdentifier<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing service identifiers from symbols to their own types is one change that could be done in a separate PR to make this one smaller. It affects a lot of files that need to be reviewed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'll see to extract this once I get back to a functional state with this PR.
Right now I only used serviceIdentifier
where I happened to work, but I could try to make a PR that replaces everything instead.
* | ||
* Most implementations are going to be API adapters. | ||
*/ | ||
export const Connection = serviceIdentifier<Connection<any>>('Connection'); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why is there a service identifier here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I used to bind named connections as part of internal wiring, having the identifier defined here helped.
* Most implementations are going to be API adapters. | ||
*/ | ||
export const Connection = serviceIdentifier<Connection<any>>('Connection'); | ||
export interface Connection<T> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What's the concrete benefit of connections being typed?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is a way to advertise what the transport supports. Most connections are Connection<any>
but as soon as you see Connection<string>
or Connection<Buffer>
you know you will need to serialize your data.
Closing for now, I'll come back to it eventually. |
electron-main
toelectron-browser
to use those new RPC components.dev-packages/sample-api
to use the new system.electron-main
tobackend
IPC communication.I open this PR as a draft because I still need to add much needed documentation and tests.
I expect from this PR to get feedback on the proposed new APIs and go forward with it. I also hope for #11011 to align with it.
This PR only covers Electron-specific communication, I left out the
backend
tofrontend
part for now. We can discuss if it should be done in this PR too or not.Here's a temporary rundown of the proposed new messaging and RPC system, until I document it properly:
The goal of the new APIs is to simplify usage by extenders, internal wiring should hold all the complexity but stay reasonable.
Connection<T>
are the heart of any communication.T
can be an object, serialization may happen eventually. Connections are often API adapters: Each and every communication library has its own API to send and receive messages but for most relevant use cases onlysendMessage
andonMessage
are required. You are only required to implement this abstraction if you wish to feed your own details into the other reusable messaging components.RpcConnection
are the highest-level abstraction to be able to setup JavaScript proxies. Note that instances are scoped to their respective remote instances, hence why there is no notion ofid
in this API. However you establish a remote connection to a specific instance, or whatever protocol you actually use, theRpcConnection
is abstracting it.ProxyProvider
are named bindings to allow binding a proxy as a regular Inversify service. Example:The API is meant to hide how proxies are setup: connection, protocol, etc... You just want a proxy to some remote runtime.
Proxies no longer have a
setClient
method: if you want to react to notifications you can add a listener on methods named likeonSomething...
. If you want a remote object to make requests and get responses, you need a new proxy.ServiceContribution
is the new API to expose remote services for proxies to be created. They are either a map ofservicePath
to a callback to get the actual instance to serve over RPC, or directly a callback that will handle the request (serviceId, serviceParams
).Inversify bindings might be more verbose now, but this is the best trade-off I was able to find to have small components that make sense. Implementation details are kept to the binding of internals. Theia extenders will most likely never have to write as much wiring code just to use what I documented so far.
Lots of implementation details.
Noteworthy:
ConnectionMultiplexer
that can run over mostConnection<any>
. Supports bi-directional channel opening.get
a binding without doing anything with it, this is because there are side-effects when resolving it (e.g. serve RPC calls).DeferredConnection
in various places: It will buffer messages until the underlying connection is ready, but I can start using it right-away to get a RPC proxy out of it synchronously.packages/core/src/common/types.ts#serviceIdentifier
.BrowserWindow
s is a pain...How to test
Run the Electron app, IPC-related features should work:
Open New Window
command.Review checklist
Reminder for reviewers