backend-app-api: fix ServiceRegistry instantiation race

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: blam <ben@blam.sh>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
Signed-off-by: Johan Haals <johan.haals@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-08-30 15:39:27 +02:00
committed by Johan Haals
parent ef875b07c3
commit 405d485788
2 changed files with 55 additions and 20 deletions
@@ -170,4 +170,31 @@ describe('ServiceRegistry', () => {
expect(await factoryB('catalog')).toBe(await factoryB('catalog'));
expect(await factoryA('catalog')).not.toBe(await factoryB('catalog'));
});
it('should not call factory functions more than once', async () => {
const innerFactory = jest.fn(async (pluginId: string) => {
return { x: 1, pluginId };
});
const factory = jest.fn(async () => innerFactory);
const myFactory = createServiceFactory({
service: ref1,
deps: {},
factory,
});
const registry = new ServiceRegistry([myFactory]);
await Promise.all([
registry.get(ref1)!('catalog')!,
registry.get(ref1)!('catalog')!,
registry.get(ref1)!('catalog')!,
registry.get(ref1)!('scaffolder')!,
registry.get(ref1)!('scaffolder')!,
]);
expect(factory).toHaveBeenCalledTimes(1);
expect(innerFactory).toHaveBeenCalledTimes(2);
expect(innerFactory).toHaveBeenCalledWith('catalog');
expect(innerFactory).toHaveBeenCalledWith('scaffolder');
});
});
@@ -22,7 +22,13 @@ import {
export class ServiceRegistry {
readonly #providedFactories: Map<string, ServiceFactory>;
readonly #loadedDefaultFactories: Map<Function, ServiceFactory>;
readonly #implementations: Map<ServiceFactory, Map<string, unknown>>;
readonly #implementations: Map<
ServiceFactory,
{
factoryFunc: Promise<FactoryFunc<unknown>>;
byPlugin: Map<string, Promise<unknown>>;
}
>;
constructor(factories: ServiceFactory<any>[]) {
this.#providedFactories = new Map(factories.map(f => [f.service.id, f]));
@@ -47,29 +53,31 @@ export class ServiceRegistry {
factory = loadedFactory;
}
let implementations = this.#implementations.get(factory);
if (implementations) {
if (implementations.has(pluginId)) {
return implementations.get(pluginId) as T;
}
} else {
implementations = new Map();
this.#implementations.set(factory, implementations);
let implementation = this.#implementations.get(factory);
if (!implementation) {
const factoryDeps = Object.fromEntries(
Object.entries(factory.deps).map(([name, serviceRef]) => [
name,
this.get(serviceRef)!, // TODO: throw
]),
);
implementation = {
factoryFunc: factory.factory(factoryDeps),
byPlugin: new Map(),
};
this.#implementations.set(factory, implementation);
}
const factoryDeps = Object.fromEntries(
Object.entries(factory.deps).map(([name, serviceRef]) => [
name,
this.get(serviceRef)!, // TODO: throw
]),
);
let result = implementation.byPlugin.get(pluginId) as Promise<any>;
if (!result) {
result = implementation.factoryFunc.then(func => func(pluginId));
const factoryFunc = await factory.factory(factoryDeps);
const implementation = await factoryFunc(pluginId);
implementation.byPlugin.set(pluginId, result);
}
implementations.set(pluginId, implementation);
return implementation as T;
return result;
};
}
}