fix: 🐛 implemented a circular dependency check in the ServiceRegistry

Signed-off-by: Marley Powell <marley.powell@exclaimer.com>
This commit is contained in:
Marley Powell
2023-08-08 14:10:19 +01:00
parent 402dd40b2a
commit 1ba3f52564
2 changed files with 134 additions and 0 deletions
@@ -330,6 +330,101 @@ describe('ServiceRegistry', () => {
);
});
it('should throw if there are shallow circular dependencies', async () => {
const refA = createServiceRef<string>({ id: 'a' });
const refB = createServiceRef<string>({ id: 'b' });
const factoryA = createServiceFactory({
service: refA,
deps: { b: refB },
factory: async ({ b }) => b,
});
const factoryB = createServiceFactory({
service: refB,
deps: { a: refA },
factory: async ({ a }) => a,
});
const registry = new ServiceRegistry([factoryA(), factoryB()]);
await expect(registry.get(refA, 'catalog')).rejects.toThrow(
"Failed to instantiate service 'a' for 'catalog' because of the following circular dependency: 'a' -> 'b' -> 'a'",
);
});
it('should throw if there are deep circular dependencies', async () => {
const refA = createServiceRef<string>({ id: 'a' });
const refB = createServiceRef<string>({ id: 'b' });
const refC = createServiceRef<string>({ id: 'c' });
const factoryA = createServiceFactory({
service: refA,
deps: { b: refB },
factory: async ({ b }) => b,
});
const factoryB = createServiceFactory({
service: refB,
deps: { c: refC },
factory: async ({ c }) => c,
});
const factoryC = createServiceFactory({
service: refC,
deps: { a: refA },
factory: async ({ a }) => a,
});
const registry = new ServiceRegistry([factoryA(), factoryB(), factoryC()]);
await expect(registry.get(refA, 'catalog')).rejects.toThrow(
"Failed to instantiate service 'a' for 'catalog' because of the following circular dependency: 'a' -> 'b' -> 'c' -> 'a'",
);
});
it('should throw if there are deep circular dependencies 2', async () => {
const refA = createServiceRef<string>({ id: 'a' });
const refB = createServiceRef<string>({ id: 'b' });
const refC = createServiceRef<string>({ id: 'c' });
const refD = createServiceRef<string>({ id: 'd' });
const factoryA = createServiceFactory({
service: refA,
deps: { b: refB },
factory: async ({ b }) => b,
});
const factoryB = createServiceFactory({
service: refB,
deps: { c: refC, d: refD },
factory: async ({ c, d }) => c + d,
});
const factoryC = createServiceFactory({
service: refC,
deps: { a: refA },
factory: async ({ a }) => a,
});
const factoryD = createServiceFactory({
service: refD,
deps: {},
factory: async () => 'd',
});
const registry = new ServiceRegistry([
factoryA(),
factoryB(),
factoryC(),
factoryD(),
]);
await expect(registry.get(refA, 'catalog')).rejects.toThrow(
"Failed to instantiate service 'a' for 'catalog' because of the following circular dependency: 'a' -> 'b' -> 'c' -> 'a'",
);
});
it('should decorate error messages thrown by the top-level factory function', async () => {
const myFactory = createServiceFactory({
service: ref1,
@@ -146,6 +146,43 @@ export class ServiceRegistry implements EnumerableServiceHolder {
}
}
#checkForCircularDeps(factory: InternalServiceFactory, pluginId: string) {
const head = factory;
const nodes = Object.values(factory.deps);
const depChain: Array<ServiceRef<unknown, 'plugin' | 'root'>> = [
head.service,
];
let node = nodes.shift();
if (node) {
depChain.push(node);
}
while (!!node && node.id !== head.service.id) {
const nodeFactory = this.#providedFactories.get(node.id);
const nodeDeps = nodeFactory?.deps;
if (nodeDeps) {
nodes.unshift(...Object.values(nodeDeps));
}
node = nodes.shift();
if (node) {
depChain.push(node);
}
}
const isCircular = node?.id === head.service.id;
if (isCircular) {
const circularDepChain = depChain.map(r => `'${r.id}'`).join(' -> ');
throw new Error(
`Failed to instantiate service '${factory.service.id}' for '${pluginId}' because of the following circular dependency: ${circularDepChain}`,
);
}
}
getServiceRefs(): ServiceRef<unknown>[] {
return Array.from(this.#providedFactories.values()).map(f => f.service);
}
@@ -156,6 +193,7 @@ export class ServiceRegistry implements EnumerableServiceHolder {
let existing = this.#rootServiceImplementations.get(factory);
if (!existing) {
this.#checkForMissingDeps(factory, pluginId);
this.#checkForCircularDeps(factory, pluginId);
const rootDeps = new Array<Promise<[name: string, impl: unknown]>>();
for (const [name, serviceRef] of Object.entries(factory.deps)) {
@@ -179,6 +217,7 @@ export class ServiceRegistry implements EnumerableServiceHolder {
let implementation = this.#implementations.get(factory);
if (!implementation) {
this.#checkForMissingDeps(factory, pluginId);
this.#checkForCircularDeps(factory, pluginId);
const rootDeps = new Array<Promise<[name: string, impl: unknown]>>();
for (const [name, serviceRef] of Object.entries(factory.deps)) {