Provided and Consumed services are now async - is it a good idea?


#1

I posted the original question here: https://github.com/atom/atom/issues/8226, as a issue, but somehow I’m not really satisfied with the “It’s not a bug, it’s a feature” answer. Not because I had to redo some code in my packages, but because I somehow found a scenario that this would really be a problem:

In Atom 1.0.3, providers and consumers API were made async - this means that, when we activate a package, if that package consumes a service, the providers of that service may, or may not, be already activated. The most problematic way, in my opinion, of this decision is that, somehow, “consumer” and “provider” creates a kind of dependency with the packages - the consumer depends on providers. In almost every other case, when we depend on another service, that service is loaded first - but now, it’s not the case. This leads to code that must adapt to the case of providers being fired after the consumer was loaded, but will only be used in a single case - the moment that the package is activated. It is more burden to package creators that, indeed, will only be used once (but things will break if this code is not properly treated).

I found a specific edge-case that is when package A is a consumer, package B is a provider and a consumer, and package C is a provider - in this case, package A should now that B is a provider and a consumer, because package B will receive information from C and forward to A. Or, in a more specific case, where’s what I said in the issue (real use-case here, I’m really trying to implement it):

The edge-case I found is the following: in my Everything plugin, I bind a searcher to a group of providers. So, the user types a query, the searcher fires this query to registered providers, then it awaits for providers to return. I’m trying to implement a “find symbols”, like Sublime’s, when you type @, it finds the symbols in the current file.

What I wanted to do, thought, is to this “find symbols” to be a consumer of another kind of providers - for now, let’s call then “Parsers”. This would be incredible for spec files, for instance, when there are no method definitions, but you could navigate through every testcase and every method - you would only need to register a new provider.

Ok, but now, the “Symbols” provider depends on “Parsers”, but it cannot know previously how many, and who they are. He will pass to Everything just what he knows it exists, and Everything will show just that - but, of course, a moment later a new Parser is registered, but the provider already sent information to its consumer…

I think this could become a big problem when there are consumers that depends on providers that are consumers too… it breaks a kind of “principle of less surprise”, that a user thinks that if his package depends on another, the other will already be loaded.


I think that, in the previous case, the problem is mostly because I’ll need to implement a kind of “this provider is not fully loaded yet and can send me more items in my search”, but the provider itself does not know it’s not fully loaded. With the API that we have today, there’s simply no way of solving this problem…


#2

Since the order of activation of packages was always undefined, I’m not sure where you could have inferred that it was guaranteed that a provider would always activate before consumers. Even before service-hub existed, when any package depended on any other package, there was always the question of “what if my package gets activated before the package I depend on?” For example, here is some code in an early version of my indentation-indicator package (a status bar widget) that has to wait until all packages are activated before checking to see if the status bar exists:

The above code was brittle in that if someone disabled and re-enabled the status-bar package, my widget wouldn’t get installed the second time. The service system improves upon this in that when the status-bar package gets re-enabled, it notifies all of its consumers.

Speaking of the status-bar package, I believe it holds the key to your conundrum. It sounds like you’re saying that:

  • Parsers provides a service that is called, once and only once, by the framework to pass the list of parsers to Symbols
  • Symbols provides a service that is called, once and only once, by the framework to pass the list of parsers and other stuff to Everything
  • Everything is passed the list of parsers and other stuff at some point
  • More parsers are added to Parsers thereby making what Everything has stored worthless

If my summation is correct, then the solution is simple … it is sitting right in the status-bar package. When a package consumes the status-bar service, it is given a reference to the StatusBar object itself. It can call methods on that object, which always updates the live status bar … no matter how its state changes.

So rather than passing static lists that do not reflect current reality down the line, you could:

  • Parsers provides a service that is called, once and only once, by the framework to pass a reference to itself to Symbols
  • Symbols provides a service that is called, once and only once, by the framework to pass a reference to itself to Everything
  • When Everything has work to do, it calls the reference it has to Symbols to get the latest list of parsers and stuff which never goes out of date

This also means that Everything doesn’t have to know anything about how Symbols does its work. It just asks Symbols for a list of stuff that it can do whatever magic it does stuff to and Symbols supplies stuff.

Additionally, it doesn’t have to be a reference to itself in each case. It could be an object of any kind or a list of objects of any kind … so long as those objects manage their own internals.

This is why The Law of Demeter is a thing …


#3

Well, I’m sure that the order of services was not undefined pre 1.0.3. The answer to the issue I posted on github said specificaly that 1.0.3 the services became async. It’s not the order of activation of packages that changed - it’s the “provided” and “consumed” only.

The problem with your approach is that I’m already doing this - but the order is wrong:

  • I hit a keystroke and Everything opens -> in this moment, Everything is activated too. There are no providers registered.

  • Some miliseconds later, Symbols activate, and fire the providedServices -> then, Everything catches this fire, registers this provider, and ask for this provider to bring results to the query.

  • Another miliseconds later, one of the Parsers fire -> then, Symbols register the parser, but it already sent to Everything its results.

This last step is a problem to me - in Everything, I inform to the user if some provider is still pending (it’s searching for results but the network is slow, or some other problem). I could keep listening for results for a specific query, but I don’t think it’s a good idea at all - it’s really strange to don’t know if all results for a specific provider are already listed. What I could do it’s to allow the provider to send an information to Everything, informing that there are more results. This would help in this case - but it will complicate a lot the API, it’ll fire the first query a bunch of times, and it’ll be useful only once - after all the packages are activated, this API will not be useful anymore.

I’m not trying to be intolerant (and please, if I’m sounding like that, I’m sorry). I understand the Law of Demeter, but this, in my opinion, is taking the Law to extreme situations - I know I should not assume anything about the order of activations, but in this case, I’m specifically saying: package A uses a service that package B provides. It’s kinda like saying I should not assume that when I ask to draw a window on the screen, it’ll be drawn - I should continually query for it to be present in the view and then begin to work on it.

This is specifically my view on this decision of making services async, and what I’m saying is that the impact of this decision should be discussed a little more. In my point of view, this will complicate simple packages and render some complex ones almost impossible to write…