backend-app-api: implement handling of default factories is ServiceRegistry

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-08-17 18:12:26 +02:00
parent eef91a2558
commit 5ef7648307
2 changed files with 93 additions and 12 deletions
@@ -26,7 +26,7 @@ const ref1 = createServiceRef<{ x: number; pluginId: string }>({
const sf1 = createServiceFactory({
service: ref1,
deps: {},
factory: async ({}) => {
factory: async () => {
return async pluginId => {
return { x: 1, pluginId };
};
@@ -39,7 +39,7 @@ const ref2 = createServiceRef<{ x: number; pluginId: string }>({
const sf2 = createServiceFactory({
service: ref2,
deps: {},
factory: async ({}) => {
factory: async () => {
return async pluginId => {
return { x: 2, pluginId };
};
@@ -48,13 +48,43 @@ const sf2 = createServiceFactory({
const sf2b = createServiceFactory({
service: ref2,
deps: {},
factory: async ({}) => {
factory: async () => {
return async pluginId => {
return { x: 22, pluginId };
};
},
});
const refDefault1 = createServiceRef<{ x: number; pluginId: string }>({
id: '1',
defaultFactory: async service =>
createServiceFactory({
service,
deps: {},
factory: async () => async pluginId => ({ x: 10, pluginId }),
}),
});
const refDefault2a = createServiceRef<{ x: number; pluginId: string }>({
id: '2a',
defaultFactory: async service =>
createServiceFactory({
service,
deps: {},
factory: async () => async pluginId => ({ x: 20, pluginId }),
}),
});
const refDefault2b = createServiceRef<{ x: number; pluginId: string }>({
id: '2b',
defaultFactory: async service =>
createServiceFactory({
service,
deps: {},
factory: async () => async pluginId => ({ x: 220, pluginId }),
}),
});
describe('ServiceRegistry', () => {
it('should return undefined if there is no factory defined', async () => {
const registry = new ServiceRegistry([]);
@@ -101,4 +131,43 @@ describe('ServiceRegistry', () => {
pluginId: 'catalog',
});
});
it('should return the defaultFactory from the ref if not provided to the registry', async () => {
const registry = new ServiceRegistry([]);
const factory = registry.get(refDefault1)!;
expect(factory).toEqual(expect.any(Function));
await expect(factory('catalog')).resolves.toEqual({
x: 10,
pluginId: 'catalog',
});
});
it('should not return the defaultFactory from the ref if provided to the registry', async () => {
const registry = new ServiceRegistry([sf1]);
const factory = registry.get(refDefault1)!;
expect(factory).toEqual(expect.any(Function));
await expect(factory('catalog')).resolves.toEqual({
x: 1,
pluginId: 'catalog',
});
});
it('should handle duplicate defaultFactories by duplicating the implementations', async () => {
const registry = new ServiceRegistry([]);
const factoryA = registry.get(refDefault2a)!;
const factoryB = registry.get(refDefault2b)!;
expect(factoryA).toEqual(expect.any(Function));
expect(factoryB).toEqual(expect.any(Function));
await expect(factoryA('catalog')).resolves.toEqual({
x: 20,
pluginId: 'catalog',
});
await expect(factoryB('catalog')).resolves.toEqual({
x: 220,
pluginId: 'catalog',
});
expect(await factoryA('catalog')).toBe(await factoryA('catalog'));
expect(await factoryB('catalog')).toBe(await factoryB('catalog'));
expect(await factoryA('catalog')).not.toBe(await factoryB('catalog'));
});
});
@@ -14,35 +14,47 @@
* limitations under the License.
*/
import {
AnyServiceFactory,
ServiceFactory,
FactoryFunc,
ServiceRef,
} from '@backstage/backend-plugin-api';
export class ServiceRegistry {
readonly #implementations: Map<string, Map<string, unknown>>;
readonly #factories: Map<string, AnyServiceFactory>;
readonly #providedFactories: Map<string, ServiceFactory>;
readonly #loadedDefaultFactories: Map<Function, ServiceFactory>;
readonly #implementations: Map<ServiceFactory, Map<string, unknown>>;
constructor(factories: AnyServiceFactory[]) {
this.#factories = new Map(factories.map(f => [f.service.id, f]));
constructor(factories: ServiceFactory<any>[]) {
this.#providedFactories = new Map(factories.map(f => [f.service.id, f]));
this.#loadedDefaultFactories = new Map();
this.#implementations = new Map();
}
get<T>(ref: ServiceRef<T>): FactoryFunc<T> | undefined {
const factory = this.#factories.get(ref.id);
if (!factory) {
let factory = this.#providedFactories.get(ref.id);
const { defaultFactory } = ref;
if (!factory && !defaultFactory) {
return undefined;
}
return async (pluginId: string): Promise<T> => {
let implementations = this.#implementations.get(ref.id);
if (!factory) {
let loadedFactory = this.#loadedDefaultFactories.get(defaultFactory!);
if (!loadedFactory) {
loadedFactory = (await defaultFactory!(ref)) as ServiceFactory;
this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory);
}
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(ref.id, implementations);
this.#implementations.set(factory, implementations);
}
const factoryDeps = Object.fromEntries(