Skip to content
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

Reactivity: Services #20095

Open
wycats opened this issue May 20, 2022 · 8 comments
Open

Reactivity: Services #20095

wycats opened this issue May 20, 2022 · 8 comments
Assignees
Labels
design A topic in the design phase

Comments

@wycats
Copy link
Member

wycats commented May 20, 2022

Some basic notes:

  • A service is "just a resource constructor" (with zero arguments)
  • An owner is a context that instantiates resources as singletons
  • An app is a special context managed by the framework (an engine is a special kind of app)
  • An object is a framework object if it's associated with an owner
  • The @service decorator can be applied to any framework object, and instantiates a resource constructor in the context of the owner associated with the framework object
@wycats wycats added the design A topic in the design phase label May 20, 2022
@wycats wycats self-assigned this May 20, 2022
@NullVoxPopuli
Copy link
Contributor

I'm going to be prototyping this out soon (not ™️), in library-space -- and the outcome of that will be

  • ergonomics / API to influence:
    • 2 RFCs:
      • owner singletons
      • Service Manager (the hard thing (lot of capabilities, testing concerns, etc))

@jelhan
Copy link
Contributor

jelhan commented Jun 13, 2022

Are there any additional information about that design? Haven't thought much about it before, but I fear it could get confusing if the a class could be used as a singleton and as a having many instances at the same time. Only depending if it is consumed via @service(MyClass) or new MyClass.

class MyService {
}

class AConsumer {
  @service(MyService) singleton;
  @service(MyService) referenceToSameSingletonInstance;

  aSecondInstance = new MyService();
  aThirdInstance = new MyService();
}

How would it integrate with current lookup API? Would this work?

get isThisSupported() {
  return getOwner(this).lookup(MyService);
}

I see how the design simplifies things a lot. But would be interested in teaching considerations and it also allows patterns, which look like a footgun.

@chriskrycho
Copy link
Contributor

@jelhan that is already the case today. Literally nothing stops you from doing this:

import Component from '@glimmer/component';
import MyService from '../services/my-service';

export default class Example extends Component {
  myService = MyService.create();
}

You shouldn’t do that, because it’s super confusing, but at the end of the day it’s just a class! (See this Twiddle for a demo of this in action.)

@ef4
Copy link
Contributor

ef4 commented Aug 2, 2022

A use case that I think needs to be explained here is specialization across package boundaries.

Consider an addon that needs a service, but doesn't have any implementation of that service. It has only an abstract base class. The addon expects apps to provide their own implementation of the service that fills in the details. IMO this is a pretty central use case for dependency injection.

I can see a few different ways to handle this case, each with some pros and cons.

Customized Module Resolving

Following normal module resolution rules (node_modules de-facto rules as understood by typescript), the addon's component cannot import the app's implementation of the service. To make that possible, we could decide to customize module resolving, such that a special path like "$app/services/store" refers abstractly to the consuming app's module namespace.

A benefit of this approach is that the module dependency graph manages to capture the real dependencies, so that the service can be loaded at an optimized time based on the set of components consuming it.

A downside of this approach is that typescript will need to be specially configured so that the addon author has valid types during development (presumably they need to map $app/services/whatever to their own abstract interface for the whatever service). I think most general-purpose javascript tooling won't be too hard to configure to support this pattern, but it will require extra config work whenever someone is trying to get a new tool to understand their code.

Another downside of this approach is that $app might not be a rich enough extension to module resolving rules. For example, you may want to consume the addon from another addon, and it's the consuming addon that provides the service. Now instead of $app/services/store you'd wish you could say something like $parent/services/store, meaning "the package that consumed me". But multiple packages might consume your addon at once, resulting in ambiguity and possible conflicts over the implementation. Maybe in this case, the consuming addon would simply need to document that apps need to manually setup the service, because the addon cannot do it. This would make conflicts apparent and give the app author the ability to resolve them, but it's more manual setup.

Runtime Override Registration

An alternative design would be to let the addon always refer to the abstract class. It would inject the abstract class. Runtime code would be responsible for mapping that request to the specialized implementation.

This implies some API for connecting the two classes. Something like an explicit override(TheBaseClass, MyImplementation) call.

The benefit of this approach is that no weird resolving rules are needed and typescript would naturally understand the addon author's intent to use the abstract interface.

The downside of this approach is that it probably forces the loading of the service (and the abstract base class) to become eager. It's no longer possible to "pull" the service implementation into the build as a natural side effect of pulling some component, etc, into the build. It's not clear how you could run the override() "just in time".

Async Construction

Another possibility would be to let service injection have an asynchronous implementation, and absorb that asynchrony while instantiating objects.

A benefit of this approach is that the vast majority of the instantiations we're talking about are done within the framework, not in user code. For example, components get instantiated by glimmer, not by your own code calling new MyComponent. So if there was a special step to wait for async injections immediately before or after the actual component construction, we could handle that internally without making users do anything.

Another benefit of this approach is that it would be aligned with browser capabilities and wouldn't necessarily need any special build time support.

A downside of this approach is that the results may be less optimized than they could be because the build tooling cannot necessarily predict what implementation will be requested at runtime.

@NullVoxPopuli
Copy link
Contributor

I prototyping this out right now!
hope it goes well!

NullVoxPopuli added a commit to NullVoxPopuli/ember-resources that referenced this issue Mar 7, 2023
Resolves #622
Experiment for: emberjs/ember.js#20095
NullVoxPopuli added a commit to NullVoxPopuli/ember-resources that referenced this issue Mar 7, 2023
Resolves #622
Experiment for: emberjs/ember.js#20095
NullVoxPopuli added a commit to NullVoxPopuli/ember-resources that referenced this issue Mar 8, 2023
Resolves #622
Experiment for: emberjs/ember.js#20095
NullVoxPopuli added a commit to NullVoxPopuli/ember-resources that referenced this issue Mar 8, 2023
Resolves #622
Experiment for: emberjs/ember.js#20095
NullVoxPopuli added a commit to NullVoxPopuli/ember-resources that referenced this issue Mar 8, 2023
Resolves #622
Experiment for: emberjs/ember.js#20095
@NullVoxPopuli
Copy link
Contributor

NullVoxPopuli commented May 20, 2023

It went ok. Lots of features we need are missing for it to be production ready, but as far as a prototype, the basics weren't bad: https://ember-resources.pages.dev/funcs/service.service

@MelSumner MelSumner moved this to In Progress in Polaris Edition of Ember Jun 13, 2023
@MelSumner MelSumner changed the title Services Reactivity: Services Jun 13, 2023
@kategengler kategengler moved this from In Progress to Needs Update in Polaris Edition of Ember Jan 22, 2024
@kategengler kategengler moved this from Needs Update to Blocked in Polaris Edition of Ember Mar 18, 2024
@kategengler
Copy link
Member

Possibly descoped from Polaris after discussion at 2024 f2f.

@NullVoxPopuli
Copy link
Contributor

Related: emberjs/tracking-polaris#18 (comment)

(since I wasn't at most of the f2f haha 🙃 -- I'll try to get myself up to date tho)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
design A topic in the design phase
Projects
Status: Blocked
Development

No branches or pull requests

6 participants