Matthew Goulart
Matthew Goulart

Reputation: 3065

Inversifyjs: passing container to another object causes binding resolution to fail

I have the following:

//Editor.TS
let values = await Promise.all([Theme.getTheme(config.theme), I18n.getI18n(config.i18n)]);

//Set up DI container
this.services = new Container();
decorate(injectable(), EventEmitter);
this.services.bind<IContext>(TYPES.Context).toConstantValue(this);
this.services.bind<Theme>(TYPES.Theme).toConstantValue(values[0]);
this.services.bind<I18n>(TYPES.I18n).toConstantValue(values[1]);
this.services.bind<any>(TYPES.Data).toConstantValue(data);
this.services.bind<Toolbox>(TYPES.Toolbox).to(Toolbox).inSingletonScope();
this.services.bind<Workspace>(TYPES.Workspace).to(Workspace).inSingletonScope();

A call to this.services.get<I18n>(TYPES.I18n); works perfefctly if it's in the same function as the code above. However, I have a plugin system where each plugin is built (with webpack) into it's own library. The idea is you pull in only the plugins you need so as not to bloat the core library with stuff people might never use.

Anyways so the plugin constructor looks like this:

//APlugin.ts
constructor(config, services: Container) {
    this.currentLang = this.defaultLang = config.defaultLanguage;
    this.supportedLangs = config.supportedLanguages;

    this.i18n = services.get<I18n>(TYPES.I18n);
    this.context = services.get<IContext>(TYPES.Context);

    //Editor Events
    this.context.on("dataGenerating", e => this.onDataGenerating());
    this.context.on("dataGenerated", data => this.onDataGenerated(data));

}

As you can see, the plugin accepts an anonymous object and a Container instance. The idea here is to allow the plugin designer to pull any dependencies they need from the container. Writing the dependencies directly in the constructor is not an option for reasons that i'd rather not go into.

The plugins are instantiated as follows:

//Editor.ts
loadPlugin(plugin): IEditorPlugin {
    let instance;

    if (Array.isArray(plugin))
        instance = new plugin[0](plugin[1], this.services);
    else
        instance = new plugin[0](this.services);

    this.plugins.push(instance);

    if (typeof instance.getMenuDom === 'function')
        this.workspace.addButton(instance.getMenuDom());

    return instance;
}

Here, the plugin parameter can be either a constructor or [constructor, config: any]

So the issue is that when the plugin's constructor is running I get the following error:

Uncaught (in promise) Error: No matching bindings found for serviceIdentifier: Symbol(I18n)

Remember that the exact same call to get, only in the same function as the container is defined works just fine.

If I debug and break on this.i18n = services.get<I18n>(TYPES.I18n); in APlugin.ts, I see: enter image description here

So it looks like the container is set up and working correctly, the _bindingDictionary property contains a reference to TYPES.I18n. I should also mention that this issue occurs with all bindings. Replacing the get<I18n> with any other bound type generates the same error.

I did some digging and it appears the point of failure is in the getBindings function: enter image description here enter image description here

The call to bindingDictionary.hasKey fails even though it's clear that the result should be true.

Upvotes: 1

Views: 2981

Answers (1)

Matthew Goulart
Matthew Goulart

Reputation: 3065

The reason is because calling Symbol(..) creates a unique symbol every time it is called regardless of the argument being the same or not.

The key was to use Symbol.for(..). This creates a symbol and stores it in the global symbol cache, or return that symbol if it already exists. This will survive across contexts.

Upvotes: 2

Related Questions