tsyringe icon indicating copy to clipboard operation
tsyringe copied to clipboard

special handling of asyncs and promises

Open xenoterracide opened this issue 4 years ago • 16 comments

so I have this code

export function context(): DependencyContainer {
  container.register(
    Beans.APOLLO_CONFIG,
    {
      useFactory: instanceCachingFactory<Promise<Config>>(async(c) => {
        return getApolloServerConfig();
      }),
    });
  return container;
}

getApolloServerConfig is an async function

now all the way down I have to do this

export function testContext(): DependencyContainer {
  const testContainer = context().createChildContainer();
  testContainer.register(
    TestBeans.APOLLO_SERVER,
    {
      useFactory: instanceCachingFactory<Promise<ApolloServer>>(async(c) => {
        const config = await c.resolve<Promise<Config>>(Beans.APOLLO_CONFIG);
        return new ApolloServer({
          ...config,
          engine: false,
        });
      }),
    });

  testContainer.register(
    TestBeans.APOLLO_TEST_CLIENT,
    {
      useFactory: instanceCachingFactory<Promise<ApolloServerTestClient>>(async(c) => {
        const server = await c.resolve<Promise<ApolloServer>>(TestBeans.APOLLO_SERVER);
        return createTestClient(server);
      }),
    });
  return testContainer;
}

including a final await in after my resolve.

what I'd really like to do (and maybe this isn't possible? is to return the promise on the first one, but have the following calls simply request the result of the first promise.

export function testContext(): DependencyContainer {
  const testContainer = context().createChildContainer();
  testContainer.register(
    TestBeans.APOLLO_SERVER,
    {
      useFactory: instanceCachingFactory<Promise<ApolloServer>>((c) => {
        const config = await c.resolve<Config>(Beans.APOLLO_CONFIG);
        return new ApolloServer({
          ...config,
          engine: false,
        });
      }),
    });

  testContainer.register(
    TestBeans.APOLLO_TEST_CLIENT,
    {
      useFactory: instanceCachingFactory<ApolloServerTestClient>((c) => {
        const server = await c.resolve<ApolloServer>(TestBeans.APOLLO_SERVER);
        return createTestClient(server);
      }),
    });
  return testContainer;
}

this may not be possible in any sort of sane rational way, I'm uncertain as I'm only recently coming to this from a Java/Spring world.

xenoterracide avatar Dec 13 '19 16:12 xenoterracide

Promises cache their values by default, so multiple calls to resolve() return the first resolution result. It's like a Java Future that way. https://www.ecma-international.org/ecma-262/6.0/#sec-promise-resolve-functions

mattbishop avatar Dec 17 '19 21:12 mattbishop

sure, but I'd rather not have to do container.resolve<Promise<Foo> when I could (in theory) just do container.resolve<Foo>

xenoterracide avatar Dec 17 '19 22:12 xenoterracide

Dependency injection is by nature synchronous, so you would need to register the Foo instance after your first resolve in that case.

Xapphire13 avatar Dec 22 '19 06:12 Xapphire13

I have come up with this as sort of a pattern, but I still question whether it might be possible to block on resolve, when registering a promise (yes I know DI is blocking by nature), basically a way of telling the container, this will be available, so wait for it...

  await createContentfulConnector(container.resolve(Beans.CONTENTFUL_CONNECTOR_CONFIG))
    .then((conn) => {
      container.register(Beans.CONTENTFUL_CONNECTOR, { useValue: conn });
    });

xenoterracide avatar Dec 23 '19 18:12 xenoterracide

As far as I know, there is no way to make asynchronous things block in JavaScript (without using a native C/C++ module). The pattern you posted above is exactly what I was thinking, but you'd still need to make sure your program awaits the resolve/register before trying to resolve the actual instance (which would be the responsibility of the program, not the DI container).

I could see a case for adding a helper for the above pattern into tsyringe though (to guide usage to the pattern)

Xapphire13 avatar Dec 23 '19 22:12 Xapphire13

I'd be ok with that, or maybe just some documentation. Though I can't help but wondering if maybe there's a way that a flag could be set internally for "building in progress" then also have that updated once the promise is resolved, in this way the resolve could be called as normal, but continue to block until ready.

another solution would be to inject proxies and do the blocking (if necessary) in there (is that possible?) though I would suggest some way of saying proxy mode is acceptable.

xenoterracide avatar Dec 27 '19 15:12 xenoterracide

I think the best way to handle this right now would be to update the documentation for this case. Even if the container internally tracked whether or not "building is in progress", there would still need to be some external await ... since we can't force JS to block. Because of that, I feel the solution should live entirely outside of the container (via the helper function you suggested for example)

Xapphire13 avatar Dec 31 '19 20:12 Xapphire13

so, I wrote this...

export function factory(explicit?: symbol, { singleton = true, container = rootContainer() } = {}) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const token = !!explicit ? explicit : Symbol.for(propertyKey);
    const factory = (c: DependencyContainer) => {
      const obj = descriptor.value.apply(target, [c]);
      if (obj instanceof Promise) {
        const handler = {
          get: (target: Promise<any>, prop: string, receiver: any) => {
            return target.then((o) => {
              return o[prop].apply(o);
            });
          },
        };
        return new Proxy(obj, handler);
      }
      return obj;
    };
    const finalFactory = singleton ? instanceCachingFactory(factory) : factory;
    container.register(token, { useFactory: finalFactory });
  };

would something like proxyForPromiseFactory(...) be a good idea to fulfill this? (this is a first attempt, if you see something broken please let me know)

xenoterracide avatar Jan 03 '20 04:01 xenoterracide

looked into the proxy thing more... it doesn't work, the more I think about this, I'm not sure it's a good idea, in our case it's me trying to support an existing anti-pattern.

xenoterracide avatar Jan 14 '20 16:01 xenoterracide

Yeah, I still think the best fix for this is to document how to handle async resolutions

Xapphire13 avatar Jan 17 '20 06:01 Xapphire13

I was reading this issue and thinking if there is a better way of doing this. Can we specify on the arguments of the factory the dependencies to build the new bean and then the framework can orchestrate to call the factory function just when all their dependencies are resolved?

Something like this:

  testContainer.register(
    TestBeans.APOLLO_TEST_CLIENT,
    {
      useFactory: instanceCachingFactory<Promise<ApolloServerTestClient>>(TestBeans.APOLLO_SERVER, (c, server: ApolloServer)
        => createTestClient(server)),
    });

luiz290788 avatar Jun 28 '20 18:06 luiz290788

@Xapphire13 sorry to jump in like this, but thread is the closest thing I've found online to help with this use case. What is your recommendation here for handling async resolutions? @xenoterracide did you find a better way? The thread hasn't been updated for a while so I wondered if things had progressed. I am using Tsyringe with routing-controllers so I need some way to resolve my async dependencies before an API request is addressed.

EDIT: For my server component I decided to switch to using Nest.js which handles asynchronous providers out the box. However, I'm still using tsyringe for my React component and it would be great if there was a way handle async resolutions.

cjmyles avatar Dec 18 '20 10:12 cjmyles

Has anyone found a solution to this? this unfortunately was a huge blocker for me :(

Fedcomp avatar May 24 '21 00:05 Fedcomp

There is a fork of tsyringe that uses asynchronous initializers as decorators https://github.com/launchtray/tsyringe

morsedev avatar Oct 17 '21 17:10 morsedev

Hitting the same issue, but I agree that DI should focus on injection, whereas async initialisation should live outside the container. My approach: I've create an abstract class AsyncService for these use cases which has a static async bindContainer(...) which first registers the component, and then calls the AbstractService's init() function:

export abstract class AsyncService {
	static async bindContainer(
		container: DependencyContainer,
		token: InjectionToken,
		provider: ClassProvider<AsyncService>,
		options?: RegistrationOptions,
	) {
	  container.register(token, provider, options);
	  await container.resolve<AsyncService>(token).init();
	}

	async init(): Promise<void> {}
}

class UserService extends AsyncService {
	constructor(
		@inject('database') protected db: Database,
	) {
	  super();
	}

         async init(): Promise<void> {
           // do some async init stuff, eg:
	  await this.db.getUser();
         }
}

// at composition root:
await UserService.bindContainer(
		container,
		UserService,
		{
			useClass: UserService,
		},
	);

Resolving the deps can be done without handling the initialisation promise. Using a static method as it's a bit easier to read when writing ConcreteService.bindContainer(...) but that could also live somewhere else.

Drawbacks:

  • Instances are not lazily loaded.
  • Won't work for Lifecycle.ResolutionScoped and Lifecycle.Transient

giorgiogross avatar Dec 09 '23 11:12 giorgiogross

Got another approach that solves the drawbacks of the one I posted before, it's mainly a wrapper around the existing API that improves readability and reduces duplicating registration-code:

  • I still have an AsyncService class that declares an async init() function
  • The an async FactoryProvider is essentially a ServiceProvider that initialises the service asynchronously. To play nicely with resolving tokens, for each AsyncService implementation I also has implement its own ServiceProvider subclass.
  • init() is called through the ServiceProvider
  • Registering the ServiceProvider is done through a convenience function that:
    • Registers the constructor through a ClassProvider under a private token - no-one can access the class instance unless using its provider
    • Registers the ServiceProvider for the passed token, which whenever it is resolved, will call await init() on the service.

This means now I can register the class provider and factory provider tied to their AsyncService implementation, with their own Lifecycle each. Every time I resolve the concrete ServiceProvider I can be sure to get the factory that properly initialises my service async, so my client is forced to await that async factory and awaiting full initialisation is done in the control flow, not during dependency injection.

export class ServiceProvider<T extends AsyncService> extends Promise<T> {}

export abstract class AsyncService {
    async init(): Promise<void> {}
}

export async function registerServiceProvider(
	container: DependencyContainer,
	providerToken: InjectionToken<ServiceProvider>,
	providedClass: constructor<AsyncService> | DelayedConstructor<AsyncService>,
	options: RegistrationOptions & {
		factoryWrapper?: (
			factoryFunc: FactoryFunction<ServiceProvider>,
		) => FactoryFunction<ServiceProvider>;
	} = {
		factoryWrapper: (f) => f,
		lifecycle: Lifecycle.Transient,
	},
) {
	const serviceUuidToken = uuidv4();
	container.register(
		serviceUuidToken,
		{
			useClass: providedClass,
		},
		{
			lifecycle: options.lifecycle,
		},
	);

	const serviceFactory: FactoryFunction<ServiceProvider> = async (
		dependencyContainer: DependencyContainer,
	) => {
		const service = dependencyContainer.resolve<AsyncService>(serviceUuidToken);
		await service.init();
		return service;
	};
	container.register<ServiceProvider>(providerToken, {
		useFactory: options.factoryWrapper
			? options.factoryWrapper(serviceFactory)
			: serviceFactory,
	});
}

// Registering a concrete implementation:
@injectable()
class ConcreteServiceProvider extends ServiceProvider<ConcreteService> {}
@injectable()
class ConcreteService  extends AsyncService {
        async init() { /* your async init */ }
}
registerServiceProvider(
		container,
		ConcreteServiceProvider,
		ConcreteService,
		{
			lifecycle: Lifecycle.ContainerScoped,
			factoryWrapper: instancePerContainerCachingFactory,
		},
	);

// Resolving the service through its provider:
const serviceProvider = container.resolve<ConcreteServiceProvider>(
	ConcreteServiceProvider,
);
const service = await serviceProvider();

It's a more generic approach than mentioned in the comments above, which allows for declaring AsyncService implementations that are resolved and initialised appropriately without re-implementing that logic for every such class you might have. It's useful in case you find yourself with many similar cases where you need async initialisation of your deps.

giorgiogross avatar Dec 13 '23 12:12 giorgiogross