refactor: implemented code review suggestions

Signed-off-by: Marley Powell <marley.powell@exclaimer.com>
This commit is contained in:
Marley Powell
2023-08-24 14:31:36 +01:00
parent 8ba316e287
commit 5c29b98d30
6 changed files with 57 additions and 88 deletions
+4 -3
View File
@@ -1,6 +1,7 @@
---
'@backstage/backend-app-api': patch
'@backstage/backend-app-api': minor
---
fix: :bug: implemented up front circular dependency check in the `BackendInitializer`
fix: :bug: updated `detectCircularDependency` in `DependencyGraph` to return circular dependencies starting from the first node
refactor!: updated `ServiceRegistry` to have a static create method and private constructor.
fix: updated `ServiceRegistry` to perform circular dependency check on creation.
fix: updated `detectCircularDependency` in `DependencyGraph` to return circular dependencies starting from the first node
@@ -324,25 +324,4 @@ describe('BackendInitializer', () => {
"Extension point registered for plugin 'testA' may not be used by module for plugin 'testB'",
);
});
it('should throw if circular dependency cycles are detected', async () => {
const refA = createServiceRef<string>({ id: 'a' });
const refB = createServiceRef<string>({ id: 'b' });
const init = new BackendInitializer([
createServiceFactory({
service: refA,
deps: { b: refB },
factory: async ({ b }) => b,
})(),
createServiceFactory({
service: refB,
deps: { a: refA },
factory: async ({ a }) => a,
})(),
]);
await expect(init.start()).rejects.toThrow(
`Circular dependencies detected:
'a' -> 'b' -> 'a'`,
);
});
});
@@ -167,13 +167,11 @@ export class BackendInitializer {
}
async #doStart(): Promise<void> {
this.#serviceHolder = new ServiceRegistry([
this.#serviceHolder = ServiceRegistry.create([
...this.#defaultApiFactories,
...this.#providedServiceFactories,
]);
this.#serviceHolder.checkForCircularDeps();
const featureDiscovery = await this.#serviceHolder.get(
featureDiscoveryServiceRef,
'root',
@@ -90,12 +90,12 @@ const refDefault2b = createServiceRef<{ x: number }>({
describe('ServiceRegistry', () => {
it('should return undefined if there is no factory defined', async () => {
const registry = new ServiceRegistry([]);
const registry = ServiceRegistry.create([]);
expect(registry.get(ref1, 'catalog')).toBe(undefined);
});
it('should return an implementation for a registered ref', async () => {
const registry = new ServiceRegistry([sf1()]);
const registry = ServiceRegistry.create([sf1()]);
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({ x: 1 });
await expect(registry.get(ref1, 'scaffolder')).resolves.toEqual({ x: 1 });
expect(await registry.get(ref1, 'catalog')).toBe(
@@ -110,7 +110,7 @@ describe('ServiceRegistry', () => {
});
it('should handle multiple factories with different serviceRefs', async () => {
const registry = new ServiceRegistry([sf1(), sf2()]);
const registry = ServiceRegistry.create([sf1(), sf2()]);
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({
x: 1,
@@ -131,7 +131,7 @@ describe('ServiceRegistry', () => {
return { x: 2 };
},
});
const registry = new ServiceRegistry([factory(), sf1()]);
const registry = ServiceRegistry.create([factory(), sf1()]);
await expect(registry.get(ref2, 'catalog')).rejects.toThrow(
"Failed to instantiate 'root' scoped service '2' because it depends on 'plugin' scoped service '1'.",
);
@@ -145,7 +145,7 @@ describe('ServiceRegistry', () => {
return { x: rootDep.x };
},
});
const registry = new ServiceRegistry([factory(), sf2()]);
const registry = ServiceRegistry.create([factory(), sf2()]);
await expect(registry.get(ref1, 'catalog')).resolves.toEqual({
x: 2,
});
@@ -160,7 +160,7 @@ describe('ServiceRegistry', () => {
return { x: rootDep.x };
},
});
const registry = new ServiceRegistry([factory(), sf2()]);
const registry = ServiceRegistry.create([factory(), sf2()]);
await expect(registry.get(ref, 'catalog')).resolves.toEqual({
x: 2,
});
@@ -175,35 +175,35 @@ describe('ServiceRegistry', () => {
return { pluginId: meta.getId() };
},
});
const registry = new ServiceRegistry([factory()]);
const registry = ServiceRegistry.create([factory()]);
await expect(registry.get(ref, 'catalog')).resolves.toEqual({
pluginId: 'catalog',
});
});
it('should use the last factory for each ref', async () => {
const registry = new ServiceRegistry([sf2(), sf2b()]);
const registry = ServiceRegistry.create([sf2(), sf2b()]);
await expect(registry.get(ref2, 'catalog')).resolves.toEqual({
x: 22,
});
});
it('should use the defaultFactory from the ref if not provided to the registry', async () => {
const registry = new ServiceRegistry([]);
const registry = ServiceRegistry.create([]);
await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({
x: 10,
});
});
it('should not use the defaultFactory from the ref if provided to the registry', async () => {
const registry = new ServiceRegistry([sf1()]);
const registry = ServiceRegistry.create([sf1()]);
await expect(registry.get(refDefault1, 'catalog')).resolves.toEqual({
x: 1,
});
});
it('should handle duplicate defaultFactories by duplicating the implementations', async () => {
const registry = new ServiceRegistry([]);
const registry = ServiceRegistry.create([]);
await expect(registry.get(refDefault2a, 'catalog')).resolves.toEqual({
x: 20,
});
@@ -234,7 +234,7 @@ describe('ServiceRegistry', () => {
defaultFactory: factoryLoader,
});
const registry = new ServiceRegistry([]);
const registry = ServiceRegistry.create([]);
await Promise.all([
expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(),
expect(registry.get(ref, 'catalog')).resolves.toBeUndefined(),
@@ -252,7 +252,7 @@ describe('ServiceRegistry', () => {
factory,
});
const registry = new ServiceRegistry([myFactory()]);
const registry = ServiceRegistry.create([myFactory()]);
await Promise.all([
registry.get(ref1, 'catalog')!,
@@ -274,7 +274,7 @@ describe('ServiceRegistry', () => {
factory,
});
const registry = new ServiceRegistry([myFactory()]);
const registry = ServiceRegistry.create([myFactory()]);
await Promise.all([
registry.get(ref1, 'catalog')!,
@@ -296,7 +296,7 @@ describe('ServiceRegistry', () => {
},
});
const registry = new ServiceRegistry([myFactory()]);
const registry = ServiceRegistry.create([myFactory()]);
await expect(registry.get(ref1, 'catalog')).rejects.toThrow(
"Failed to instantiate service '1' for 'catalog' because the following dependent services are missing: '2'",
@@ -323,7 +323,7 @@ describe('ServiceRegistry', () => {
},
});
const registry = new ServiceRegistry([factoryA(), factoryB()]);
const registry = ServiceRegistry.create([factoryA(), factoryB()]);
await expect(registry.get(refA, 'catalog')).rejects.toThrow(
"Failed to instantiate service 'a' for 'catalog' because the factory function threw an error, Error: Failed to instantiate service 'b' for 'catalog' because the following dependent services are missing: 'c', 'd'",
@@ -347,9 +347,7 @@ describe('ServiceRegistry', () => {
factory: async ({ a }) => a,
});
const registry = new ServiceRegistry([factoryA(), factoryB()]);
expect(() => registry.checkForCircularDeps()).toThrow(
expect(() => ServiceRegistry.create([factoryA(), factoryB()])).toThrow(
`Circular dependencies detected:
'a' -> 'b' -> 'a'`,
);
@@ -385,14 +383,14 @@ describe('ServiceRegistry', () => {
factory: async ({ c }) => c,
});
const registry = new ServiceRegistry([
factoryA(),
factoryB(),
factoryC(),
factoryD(),
]);
expect(() => registry.checkForCircularDeps()).toThrow(
expect(() =>
ServiceRegistry.create([
factoryA(),
factoryB(),
factoryC(),
factoryD(),
]),
).toThrow(
`Circular dependencies detected:
'a' -> 'b' -> 'a'
'c' -> 'd' -> 'c'`,
@@ -422,13 +420,9 @@ describe('ServiceRegistry', () => {
factory: async ({ a }) => a,
});
const registry = new ServiceRegistry([
factoryA(),
factoryB(),
factoryC(),
]);
expect(() => registry.checkForCircularDeps()).toThrow(
expect(() =>
ServiceRegistry.create([factoryA(), factoryB(), factoryC()]),
).toThrow(
`Circular dependencies detected:
'a' -> 'b' -> 'c' -> 'a'`,
);
@@ -464,14 +458,14 @@ describe('ServiceRegistry', () => {
factory: async () => 'd',
});
const registry = new ServiceRegistry([
factoryA(),
factoryB(),
factoryC(),
factoryD(),
]);
expect(() => registry.checkForCircularDeps()).toThrow(
expect(() =>
ServiceRegistry.create([
factoryA(),
factoryB(),
factoryC(),
factoryD(),
]),
).toThrow(
`Circular dependencies detected:
'a' -> 'b' -> 'c' -> 'a'`,
);
@@ -500,13 +494,9 @@ describe('ServiceRegistry', () => {
factory: async ({ a }) => a,
});
const registry = new ServiceRegistry([
factoryA(),
factoryB(),
factoryC(),
]);
expect(() => registry.checkForCircularDeps()).toThrow(
expect(() =>
ServiceRegistry.create([factoryA(), factoryB(), factoryC()]),
).toThrow(
`Circular dependencies detected:
'a' -> 'c' -> 'a'`,
);
@@ -535,13 +525,9 @@ describe('ServiceRegistry', () => {
factory: async ({ b }) => b,
});
const registry = new ServiceRegistry([
factoryA(),
factoryB(),
factoryC(),
]);
expect(() => registry.checkForCircularDeps()).toThrow(
expect(() =>
ServiceRegistry.create([factoryA(), factoryB(), factoryC()]),
).toThrow(
`Circular dependencies detected:
'b' -> 'c' -> 'b'`,
);
@@ -560,7 +546,7 @@ describe('ServiceRegistry', () => {
},
});
const registry = new ServiceRegistry([myFactory()]);
const registry = ServiceRegistry.create([myFactory()]);
await expect(registry.get(ref1, 'catalog')).rejects.toThrow(
"Failed to instantiate service '1' because createRootContext threw an error, Error: top-level error",
@@ -576,7 +562,7 @@ describe('ServiceRegistry', () => {
},
});
const registry = new ServiceRegistry([myFactory()]);
const registry = ServiceRegistry.create([myFactory()]);
await expect(registry.get(ref1, 'catalog')).rejects.toThrow(
"Failed to instantiate service '1' for 'catalog' because the factory function threw an error, Error: error in plugin",
@@ -591,7 +577,7 @@ describe('ServiceRegistry', () => {
},
});
const registry = new ServiceRegistry([]);
const registry = ServiceRegistry.create([]);
await expect(registry.get(ref, 'catalog')).rejects.toThrow(
"Failed to instantiate service '1' because the default factory loader threw an error, Error: default factory error",
@@ -58,6 +58,12 @@ const pluginMetadataServiceFactory = createServiceFactory(
);
export class ServiceRegistry implements EnumerableServiceHolder {
static create(factories: Array<ServiceFactory>): EnumerableServiceHolder {
const registry = new ServiceRegistry(factories);
registry.checkForCircularDeps();
return registry;
}
readonly #providedFactories: Map<string, InternalServiceFactory>;
readonly #loadedDefaultFactories: Map<
Function,
@@ -76,7 +82,7 @@ export class ServiceRegistry implements EnumerableServiceHolder {
>();
readonly #dependencyGraph: DependencyGraph<string>;
constructor(factories: Array<ServiceFactory>) {
private constructor(factories: Array<ServiceFactory>) {
this.#providedFactories = new Map(
factories.map(sf => [sf.service.id, toInternalServiceFactory(sf)]),
);
@@ -39,7 +39,6 @@ export interface CreateSpecializedBackendOptions {
export interface ServiceHolder {
get<T>(api: ServiceRef<T>, pluginId: string): Promise<T> | undefined;
checkForCircularDeps(): void;
}
/**