-
-
Notifications
You must be signed in to change notification settings - Fork 169
Description
Have you checked for existing feature requests?
- Completed
Summary
Suppose package1 has this in its package.json:
{
"providedServices": {
"my-service": {
"description": "Does a useful thing",
"versions": {
"1.0.0": "provideMyServiceV1",
"2.0.0": "provideMyServiceV2"
}
}
}
}And package2 has this in its package.json:
{
"consumedServices": {
"my-service": {
"versions": {
"^1.0.0": "consumeMyServiceV1",
"^2.0.0": "consumeMyServiceV2"
}
}
}
}When both packages have been activated, Pulsar will see that it can match up a provider and a consumer. Annoyingly, though, it'll make the introduction twice: once for version 1 and once for version 2 of my-service. Worse, there's nothing inherent in the service lifecycle that allows package2 to even recognize that it's being matched up with the same provider both times. If a consuming package wants to detect this scenario and ensure it consumes only V2 of the service from package1, then the service contract itself must include extra metadata (like the providing package's name). This is frustrating.
Pulsar could detect this case when making the introduction. But service-hub isn't written in such a way as to make this easy:
serviceHub.provide('my-service', '1.0.0', { foo: 'bar' });
serviceHub.provide('my-service', '2.0.0', { baz: 'thud' });You can see here that the arguments are service name, method name, and service object. So in giving this job to service-hub, we're stripping the context that would allow it to detect this situation — not that it would know how to handle it if that context were present.
But it does do something that we could perhaps build upon. Here's how we actually call service-hub when declaring what a package provides:
// `service-hub` lets you specify multiple versions of the same service at once:
serviceHub.provide('my-service', {
'1.0.0': { foo: 'bar' },
'2.0.0': { baz: 'thud' }
});This is helpful because service-hub doesn't just loop through the object and treat it like a shorthand of the example above. Instead, it saves the object as-is and treats it as a group.
And if the call to consume were written like this, we'd get the behavior we want:
serviceHub.consume('my-service', '>=1', (serviceObj) => doSomethingWithServiceObject(serviceObj))That's because, in this example, a single version string is written to match multiple candidate versions. But service-hub knows it can pick only one version to fulfill it, so it chooses version 2.0.0.
But it doesn't work like this when consuming package2’s package.json as written above. Pulsar iterates through each of the consumed services and calls serviceHub.consume on each one in turn.
What we'd probably want is a usage like this:
serviceHub.consume('my-service', {
'1.0.0': (serviceObj) => doSomethingWithServiceObject(serviceObj),
'2.0.0': (serviceObj) => doSomethingElseWithServiceObject(serviceObj)
});This isn't yet implemented, but it would do what we want here. Even though each handler is a different function, it would allow us to group them and ask that only the best match be invoked.
What benefits does this feature provide?
We want service providers and service consumers to be accommodating and accept a wide range of service versions. But if both sides of the service try to do this, they'll necessarily run into situations where the same consumer/provider pair is consumed multiple times.
In practice, this causes pain for atom-languageclient; since we're adding new features to autocomplete.provider, we want to be able to distinguish which version of the service is being asked for so we can provide a service object that works as expected. But it's practically impossible to do that without introducing a chance of redundancies. In practice, this would manifest as the same suggestion showing up two or three times in the menu.
The main drawback is that it would change existing behavior. This is why I've opened this ticket against Pulsar itself rather than against service-hub, even though the fix would be made on service-hub itself. I cannot imagine that any package is relying on this very strange behavior, but neither can I rule it out. Still, if it ends up breaking someone's workflow, I'll take the heat for it, and do whatever is needed to atone for it.
Any alternatives?
To use autocomplete.provider as an example: we could enhance the service contract to mandate that the package name and explicit version number be specified in the service object itself as metadata. But this data isn't present on autocomplete.provider right now, so this does nothing to fix the problem of backward-compatibility.
I'm reflecting on this because I'm rewriting the documentation on services to be less confusing. I want to show the reader some real-world scenarios, but if I explain this in enough detail, then this dilemma is going to be obvious. I feel silly telling the reader “yeah, this sucks, but you can get around it if you write enough code.” Better to fix the problem so that the documentation can be simplified.
Other examples:
VS Code has “dynamic registration” — I think that's the vague equivalent of service matchmaking between packages? I could research it, but I think it's probably different enough so as not to lend us any insight here.