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

Asynchronous injection of a shared module requires signature changes #475

Open
Pyo25 opened this issue Jan 6, 2017 · 7 comments
Open

Asynchronous injection of a shared module requires signature changes #475

Pyo25 opened this issue Jan 6, 2017 · 7 comments

Comments

@Pyo25
Copy link
Contributor

Pyo25 commented Jan 6, 2017

I read carefully the documentation about provider and some other Github issues (typically #418). It seems the only way to inject an asynchronous module is to inject a provider and then call this provider to get the actual module. (For instance: http://stackoverflow.com/questions/40880447/how-to-inject-an-asynchronous-dependency-in-inversify)

However it requires the functions to be async and thus return a promise.

In my use-case we're defining a Config class that needs to be initialized asynchronously (typically, the config will fetch values from an external service). Almost all the other modules require this config. Therefore it is not a viable solution to change all the public methods into async methods just to be able to use the await this.init(); trick.

I took the Inversify basic example and I extended it to show this use-case:
https://github.com/Pyo25/inversify-basic-example/tree/asynchronous_config_dependency

Particularly, the EpicBattle class has to return a Promise<string> but that something we should avoid. (cfr: https://github.com/Pyo25/inversify-basic-example/blob/asynchronous_config_dependency/src/entities/battle/epic_battle.ts)

So, my questions are:

  • is it the only way to inject an asynchronous dependency?
  • do you see any workaround we could use?

Thanks for your help

@remojansen
Copy link
Member

A provider is asynchronous so there is no other way. It sounds to me like you want to execute asynchronous code as if it was synchronous but that is not a good practice. An option is to fetch that data before InversifyJS is initialized. Once you have fetched the data you can create a binding with toConstantValue. It sounds to me like this is the right solution because you mentioned:

Almost all the other modules require this config.

This is an indicator of the config being almost like a global. It makes sense to fetch it before the app starts running.

@Pyo25
Copy link
Contributor Author

Pyo25 commented Jan 10, 2017

Thanks for the reply @remojansen.
Based on your suggestion, I see two possibilities: using a constant value or using a singleton.
The drawback I see of using a constant value is that we spread bindings over 2 different files (index.ts and inversify.config.ts).

What do you think?

With a constant value

// ./index.ts
import "reflect-metadata";
import { IConfig } from "./config";
import { Config } from "./config/config";
import { container } from "./inversify.config";
import { IService} from "./service";
import { TYPES } from "./types";

let config = new Config();

config.initialize()
  .then((cfg) => container.bind<IConfig>(TYPES.IConfig).toConstantValue(cfg))
  .then(() => container.get<IService>(TYPES.IService).initialize())
  .catch((err) => {
    console.error("fatal error", err);
    process.exit(1);
  });

// ./inversify.config.ts
// nothing about IConfig nor Config

With a singleton

// ./index.ts
import "reflect-metadata";
import { IConfig } from "./config";
import { container } from "./inversify.config";
import { IService} from "./service";
import { TYPES } from "./types";

let config = container.get<IConfig>(TYPES.IConfig);

config.initialize()
  .then(() => container.get<IService>(TYPES.IService).initialize())
  .catch((err) => {
    console.error("fatal error", err);
    process.exit(1);
  });

// ./inversify.config.ts
// [import & cie]
container.bind<IConfig>(TYPES.IConfig).to(Config).inSingletonScope();
// [other bindings]

@ajeffrey
Copy link

ajeffrey commented Jan 12, 2017

what about having the config be blank by default, loaded with values by another class, then proceeding to start up your app?

e.g.

// config.ts
@injectable()
export class Config implements IConfig {
  private _values: {};
  constructor() {
    this._values = {};
  }
  // stuff
  load(values: {}) {
    this._values = values;
  }
  // stuff
};

// config-loader.ts
@injectable()
export class ConfigLoader implements IConfigLoader {
  constructor(@inject('IConfig') private _config) {}
  async load() {
    // pull config down, asynchronously
    this._config.load(somedata);
  }
};

// ./index.ts
const loader = container.get<IConfigLoader>(TYPES.IConfigLoader);
loader.load().then(() => {
  // config is now populated, we can use classes that depend on it
});

// ./inversify.config.ts
// [import & cie]
container.bind<IConfig>(TYPES.IConfig).to(Config).inSingletonScope();
container.bind<IConfigLoader>(TYPES.IConfigLoader).to(ConfigLoader);
// [other bindings]

that way the only thing you have to change is your startup script. You can also avoid having to dirty your application logic with DI code.

@Pyo25
Copy link
Contributor Author

Pyo25 commented Jan 16, 2017

Hi @ajeffrey! Your solution seems nice as well. Thx for proposing it.

On my side I finally went for the singleton option (without a loader).

@bal1anD
Copy link

bal1anD commented Mar 25, 2019

Hi,

I read through this issue and I wanted to check to see if anyone has found a better way to initialize a singleton with a config. This is my use case:

   class DatabaseClient {
         private config: ConfigClass
         private client: MySqlClient
         constructor ( @inject('dbConfig') config: ConfigClass) {
               this.config = config
              this.config.getConfig('MYSQL').then((res) => {
                    this.client = MySql(this.config)
              })
         }
        ...
  }

Is there a way for me to avoid the this.config.getConfig() and instead inject the actual values directly ?

@parisholley
Copy link
Contributor

@Pyo25 check out #1074

@Farenheith
Copy link

Farenheith commented Jan 11, 2020

What about this solution:

An injected properties blank object:

bind(TYPES.settings).toConstantValue({});

And a SettingsLoader:

@injectable
class SettingsLoader() {
constructor(@inject(TYPES.Settings) private readonly settings: settings) { }

async load() {
  // Loads properties from external source and fills it in settings
  this.settings.config1 = value1;
  this.settings.config2 = value 2;
 }
}

this loader is injected as singleton

bind(TYPES.SettingsLoader).to(SettingsLoader).inSingletonScope();

So, in the initialization of your app, you can call the load method to fill all the settings properties.
Also, you can create some routine to reload the settings periodically, for example. This is great to make some settings be changeable externally without the need to restart your app

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

6 participants