loopbackio / loopback-next

LoopBack makes it easy to build modern API applications that require complex integrations.
https://loopback.io
Other
4.97k stars 1.07k forks source link

Allow scopes for bound dynamic values in IoC Container #228

Closed raymondfeng closed 7 years ago

raymondfeng commented 7 years ago

Description/Steps to reproduce

When a dynamic value is bound to the container, there may be different scopes to use the same instance as follows:

  1. Singleton: the bound service is completely stateless and no request dependency (for example, a utility such as password hashing) is needed. For optimal performance/memory footprint, we'll only create the instance once and reuse it within the same context.

  2. Transient: the bound service requires a new instance per request.

  3. Pooled: the bound service (for example, a repository that has a connection pool to a DB) cannot be shared but it's expensive to instantiate/setup. We can maintain a pool of such instances.

See:

Expected result

Additional information

bajtos commented 7 years ago

Singleton: the bound service is completely stateless and no request dependency (for example, a utility such as password hashing) is needed. For optimal performance/memory footprint, we'll only create the instance once and reuse it within the same context.

FWIW, this can be already achieved by creating the singleton instance at binding time.

const createDependency = () => { /*...*/ };

// static/singleton
const singleton = createDependency();
ctx.bind('foo').to(singleton);

Transient: the bound service requires a new instance per request.

This is already available AFAICT.

const createDependency = () => { /*...*/ };

// dynamic/per-request
ctx.bind('foo').toDynamicValue(createDependency);

Pooled: the bound service (for example, a repository that has a connection pool to a DB) cannot be shared but it's expensive to instantiate/setup. We can maintain a pool of such instances.

This is an interesting use case. To support it, we need some mechanism allowing dependency consumers to release the resource after the operation has completed. Since the code using @inject should not be aware of how the dependencies are created (this is by design), I think our only option is to release pooled instances when the request handler finished and the whole context can be destroyed.

// inside request handler
ctx = new Context(app);
ctx.bind('request').to('request');
// ...
await invoke();
ctx.release();

This may be useful for all objects, not only for expensive resources like database connections. In Fastify, they claim to get ~10% speed up by caching regular objects and functions via https://www.npmjs.com/package/reusify

raymondfeng commented 7 years ago

@bajtos I guess the term singleton is a bit misleading. Maybe it should be called context-singleton scope: there is only one instance within the context.

Additionally, it's not ideal to pre-create the singleton and bind it to the context. Singleton instance should be able to leverage dependency injection too. For example:

class Authenticator {
  constructor(private @inject('loginProvider') loginProvider, 
    private @inject('tokenGenerator') tokenGenerator, ...) {
  }
}
bajtos commented 7 years ago

Closing as done - see #385.