diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts index e698ade348..0e1d8614f9 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts @@ -330,6 +330,101 @@ describe('ServiceRegistry', () => { ); }); + it('should throw if there are shallow circular dependencies', async () => { + const refA = createServiceRef({ id: 'a' }); + const refB = createServiceRef({ 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({ id: 'a' }); + const refB = createServiceRef({ id: 'b' }); + const refC = createServiceRef({ 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({ id: 'a' }); + const refB = createServiceRef({ id: 'b' }); + const refC = createServiceRef({ id: 'c' }); + const refD = createServiceRef({ 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, diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index b5b55f67cf..de26619816 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -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> = [ + 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[] { 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>(); 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>(); for (const [name, serviceRef] of Object.entries(factory.deps)) {