Sp-dev-docs: ServiceKey is not reused, resulting in custom services not being able to share "data"

Created on 28 Feb 2018  路  26Comments  路  Source: SharePoint/sp-dev-docs

Category

  • [X] Question
  • [ ] Typo
  • [X] Bug
  • [ ] Additional article idea

Expected or Desired Behavior

When creating a custom service in SPFx I expect that when consuming a service I would get it as a singleton so that the first one consuming it would create it and the rest just [...] consuming it. For instance if I have a custom service with a counter, the first web part would get number 1, the second web part number 2 etc.

Observed Behavior

When consuming a custom service an instance of the service is created per Web Part/extension. In the counter example above, when adding a web part I get 1 as response, and when adding a second web part of the same type I get 2. But when I add another web part (another implementation) I start over on number 1.

image

I would have expected the numbers to be 1,2,3,4,5...

I though the idea of the ServiceKey to be "static" and not creating multiple services. See this debug view of the actual service registrations. There's two of them! :-)

image

Am I holding it wrong? Or is this not how it is supposed to work -> I need to start polluting the global name space?

Steps to Reproduce

Clone this repository and run in the workbench: https://github.com/wictorwilen/spfx-svctest

Author Feedback no-recent-activity tracked question

Most helpful comment

@waldekmastykarz Just submitted a pull request to the docs to just remove the advice on using Services and Libraries, as they do not function as perceived and that particular documentation doesn't mention that Libraries aren't supported outside the workbench. It may be worth modifying the pull request to have a blurb mentioning that services and libraries are being developed and should not be used until a future release. Your blog posts? Yeah. Right at the top. Through reading your blog posts and others referencing them, I spent a full day trying different possibilities to have an external UMD module being a SPFx library, or otherwise just be able to have shared code that webparts could use to no avail.

All external modules get loaded once for each webpart referencing them, breaking classic singleton use and requiring a workaround to ensure that out of order loading doesn't break functionality. Possible? sure. Definitely not a typical pattern and shouldn't be made any sort of official guidance.

All 26 comments

If I'm not mistaken the reason for it is, that in your case the service is included in each bundle. Despite the same key, each bundle creates its own instance rather than sharing the existing instance. For a service to work as a singleton, it would have to be packaged separately which can be accomplished only using libraries which currently aren't available to third parties.

That's what I thought as well first, but I bundled the webparts and the service into same bundle - with the same results. Yes, library works - but I haven't heard or seen anything on official support for this for many moons now...

Have you tried looking at the webpack-generated code how the service is used and instantiated there?

No I haven't - my thinking was that there's a reason behind having the key...

I've just had a quick look at it and seems like the culprit is the module system rather than the service infrastructure. If you set the breakpoint on:

export const CounterServieKey: ServiceKey<ICounterService> = ServiceKey.create<ICounterService>('WW:CounterService', CounterService);

you see it's hit twice, each time the const is referenced in your web parts. That line basically says to create the service. Internally, services keep a counter and due to creating the service multiple times the counter for your service doesn't match its name which is why a new instance is created for new type of web part.

As far as I can tell, the only way around it, is using libraries which ensure that the service is instantiated only once.

Another thing that you could try is to create an extension or a web part without UI that would register the service only once on the page. I'm not sure however if the execution order would be correct so that the service is registered before it's consumed 馃槙

Exactly, a bit of my point. Services as of now doesn't really make sense, since we can't use them, since we don't have libraries.

Workaround is of course to generate the service key once and store it in the global scope- but that'll make all of the JavaScript puritans cry :-)

export const CounterServieKey = (): ServiceKey<ICounterService> => {
    if (!(<any>window).__serviceKey) {
        (<any>window).__serviceKey = ServiceKey.create<ICounterService>('WW:CounterService', CounterService);
    }
    return (<any>window).__serviceKey;
}

Been running into this same issue, trying to find the best (or just working) way to have singletons, share code, etc. Gotta say this is the most beta area of the framework I've run into so far. The documentation on this subject is misleading, as at best it tells you at the end or in a note that it's not supported outside the workbench.

@wictorwilen if you determine a route to share code, have singleton services, etc. that actually works - I'm all ears.

@waldekmastykarz thanks for your blog posts on this. They're the foremost best information on at least getting close to having them working... Do you think you could go to the top of all of them and put in big bold letters that this isn't supported currently?

@jonthenerd you mean to the top of my blog posts or the official docs? 馃槈

As I mentioned, you don't have much choice at the moment other than a no-UI web part or an extension with the caveat that you don't have the control of the order in which it loads.

What would you like to see in the docs ideally?

@waldekmastykarz Just submitted a pull request to the docs to just remove the advice on using Services and Libraries, as they do not function as perceived and that particular documentation doesn't mention that Libraries aren't supported outside the workbench. It may be worth modifying the pull request to have a blurb mentioning that services and libraries are being developed and should not be used until a future release. Your blog posts? Yeah. Right at the top. Through reading your blog posts and others referencing them, I spent a full day trying different possibilities to have an external UMD module being a SPFx library, or otherwise just be able to have shared code that webparts could use to no avail.

All external modules get loaded once for each webpart referencing them, breaking classic singleton use and requiring a workaround to ensure that out of order loading doesn't break functionality. Possible? sure. Definitely not a typical pattern and shouldn't be made any sort of official guidance.

Here's the ugly workaround for ya: https://github.com/wictorwilen/spfx-svctest/blob/unsupportedbutworks/src/CounterService.ts

(and this is essentially how I THINK it should work)

Thanks @wictorwilen . Probably how it _ought_ to work is that dependent modules shouldn't be loaded more than once per page, so this issue would never occur and no code change on service creation would be needed. Your particular workaround I'd be a little wary of using just do to the use of internal properties which could be changed, though perhaps this isn't an issue if the same version of SPFx continues to be loaded... (gotta love the use of underscore to mark private, even though it's advised against by many). An alternative could be to check for the presence of a window scope variable that would contain the service key, if it's not present then create the service key and store it, then return the value of the window scope variable. Since none of this code is asynchronous, it out to work consistently and not break on a SPFx version upgrade.

@jonthenerd I know - that's why the branch is called unsupportedbutworks and I only spent 4-5 minutes on it :-) - just showcasing how I think it should be work (not implemented). There's a reason we have a key and a service concept.

@wictorwilen LOL agreed. Was just trying to be clear so that the uninformed don't take your workaround and run with it.

When I first saw services I wasn't quite sure either what the purpose of the key was, when it's not used for retrieving services. It would make perfect sense to lookup services based on their key rather than a counter.

Exactly. There are so many use cases for this and I really would like to see an official statement on this topic...or if I have to roll my own service framework.

We should improve the documentation for the ServiceScope API, since its goals and requirements can sometimes be nonobvious or counterintuitive. The underlying idea is to ensure that when unknown components are dynamically loaded into the system, they can participate in consuming/providing services without causing weird bugs or nondeterministic behavior. (As one example, cyclic references are difficult to anticipate when mixing together unknown components, so ServiceScope provides for it whereas other dependency injectors tend to treat a cyclic reference as an error.)

A little background

One of the axioms is that when you ask for a service (i.e. ServiceScope.consume()), an object will always be returned which implements the contract. It may be something very trivial (e.g. a logger that discards all the log messages), but it will never be null or undefined. This avoids a whole category of if (!service) { /* do something else */ } logic that would otherwise clutter the usage patterns.

This is implemented by requiring every ServiceKey to include a default implementation. In other words, if you can name the thing at all, then you can make an instance of it.

So, the problem with multiple equivalent definitions of a ServiceKey is that they could have different default implementations. Since the default instance is shared (for performance reasons), this would mean a race condition where the first person to ask ends up deciding the implementation. That would go against another axiom of ServiceScope: The order in which you ask the questions will never affect the answers that you receive, i.e. the services at each level of the tree are unaffected by the ordering of your calls to ServiceScope.consume() at any level of tree. (Obviously autocreated services get constructed only when you ask for them, but their location in the tree and choice of implementation is fully deterministic.)

Annoying answer

The recommended practice is to declare your ServiceKey in a way such that all consumers will see the same object instance. SPFx does this by virtue of having singleton instances of our bundles loaded on the page. For third parties, the ServiceKey needs to be somehow located in a script that only gets loaded once. Our mechanisms for doing that today aren't as rich as they should be, but that's a separate GitHub issue. :-)

We of course considered defining a special category of ServiceKey that does not support autocreation, and therefore could be easily redeclared by its consumers. However, this would bring along other complications, so I'd view it as a last resort rather than a foregone conclusion.

@mpasarin @VesaJuvonen

Thanks for the elaborate answer @pgonzal - I get how it works and I was trying to use the "annoying" answer way of doing that, in a custom library component. However it doesn't really work with creating a library component (see #1292), since you need to reference the @ microsoft/sp-* components in your library.

Was thinking the same thing @wictorwilen. Thanks for the answer @pgonzal and you're right - once external dependency loading works properly, ServiceScope should come back into play.

For my current project, we've just given up having an external library be a dependency that is only loaded once and rolled everything together into one solution. I'm exploring having an ApplicationCustomizer extension do my shared logic/services and exposes a few hooks out on a window attached property, but will need to wire up my own promise/callback fun for getting past not being able to guarantee what loads in first.

Another thought that occurred to me and I was curious if you'd tried - does SPComponentLoader only load an external dependency once? Thought it was worth looking into, but as I said our current direction won't need it but you might look there (or already have).

I've had the same issue and solved it by not counting on the internal number pointer but rather create my own pointer. A lite adaption to the code from @wictorwilen

Create Service Key from custom class with method that returns:

// Method "createOwn" of custom class 
return new (<any>ServiceKey)(name, name, (serviceScope: ServiceScope): void => {
            return new serviceClass(serviceScope);
        });
// ...
public static readonly serviceKey: ServiceKey<IMyService> = ServiceKeyExt.createOwn<IMyService>('my:IMyService', MyService);

That way, the service will always be found by its Name and not the counter variable. I have 3 webparts that use the same service with shared service instance data (using parent ServiceScope).

@CedricOettel / @wictorwilen FYI this code is bypassing the type system to call the private constructor for the ServiceKey class. Please be aware that Microsoft's support and compatibility guarantees do not apply to third-party components that rely on non-public APIs. If a future change to SPFx causes your component to become unstable or broken, you'll have to sort that out on your own.

A better hack would be to find a way to share a properly constructed ServiceKey object across bundles.

@pgonzal I know, I know - a fix for #1292 or Library component will solve it, meanwhile we'll use hacks ;-)

Doing some issue house cleaning... consider this issue resolved? Seems answered... ???

To my knowledge, this is still broken. I haven't recently tested it though. I'm hoping this is fixed with the upcoming functionality allowing spfx libraries to work, mentioned the other day in the pnp call.

This issue has been automatically marked as stale because it has marked as requiring author feedback but has not had any activity for 7 days. It will be closed if no further activity occurs within next 7 days of this comment. Thank you for your contributions to SharePoint Developer activities.

Closing issue due no response from original author. If this issue is still occurring, please open a new issue with additional details. Notice that if you have included another related issue as additional comment on this, please open that also as separate issue, so that we can track it independently.

Issues that have been closed & had no follow-up activity for at least 7 days are automatically locked. Please refer to our wiki for more details, including how to remediate this action if you feel this was done prematurely or in error: Issue List: Our approach to locked issues

Was this page helpful?
0 / 5 - 0 ratings