diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts index 405a2fc630..e698ade348 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.test.ts @@ -29,9 +29,7 @@ const sf1 = createServiceFactory({ service: ref1, deps: {}, async factory() { - return async () => { - return { x: 1 }; - }; + return { x: 1 }; }, })(); @@ -61,7 +59,7 @@ const refDefault1 = createServiceRef<{ x: number }>({ service, deps: {}, async factory() { - return async () => ({ x: 10 }); + return { x: 10 }; }, })(), }); @@ -73,7 +71,7 @@ const refDefault2a = createServiceRef<{ x: number }>({ service, deps: {}, async factory() { - return async () => ({ x: 20 }); + return { x: 20 }; }, }), }); @@ -85,7 +83,7 @@ const refDefault2b = createServiceRef<{ x: number }>({ service, deps: {}, async factory() { - return async () => ({ x: 220 }); + return { x: 220 }; }, }), }); @@ -144,7 +142,7 @@ describe('ServiceRegistry', () => { service: ref1, deps: { rootDep: ref2 }, factory: async ({ rootDep }) => { - return async () => ({ x: rootDep.x }); + return { x: rootDep.x }; }, }); const registry = new ServiceRegistry([factory(), sf2]); @@ -173,8 +171,8 @@ describe('ServiceRegistry', () => { const factory = createServiceFactory({ service: ref, deps: { meta: coreServices.pluginMetadata }, - async factory() { - return async ({ meta }) => ({ pluginId: meta.getId() }); + async factory({ meta }) { + return { pluginId: meta.getId() }; }, }); const registry = new ServiceRegistry([factory()]); @@ -224,13 +222,11 @@ describe('ServiceRegistry', () => { }); it('should only call each default factory loader once', async () => { - const factoryLoader = jest.fn(async (service: ServiceRef) => + const factoryLoader = jest.fn(async (service: ServiceRef) => createServiceFactory({ service, deps: {}, - async factory() { - return async () => {}; - }, + async factory() {}, }), ); const ref = createServiceRef({ @@ -247,10 +243,31 @@ describe('ServiceRegistry', () => { }); it('should not call factory functions more than once', async () => { - const innerFactory = jest.fn(async () => { - return { x: 1 }; + const createRootContext = jest.fn(async () => ({ x: 1 })); + const factory = jest.fn(async () => ({ x: 1 })); + const myFactory = createServiceFactory({ + service: ref1, + deps: {}, + createRootContext, + factory, }); - const factory = jest.fn(async () => innerFactory); + + 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(createRootContext).toHaveBeenCalledTimes(1); + expect(factory).toHaveBeenCalledTimes(2); + }); + + it('should not call factory functions more than once without root context', async () => { + const factory = jest.fn(async () => ({ x: 1 })); const myFactory = createServiceFactory({ service: ref1, deps: {}, @@ -267,8 +284,7 @@ describe('ServiceRegistry', () => { registry.get(ref1, 'scaffolder')!, ]); - expect(factory).toHaveBeenCalledTimes(1); - expect(innerFactory).toHaveBeenCalledTimes(2); + expect(factory).toHaveBeenCalledTimes(2); }); it('should throw if dependencies are not available', async () => { @@ -296,9 +312,7 @@ describe('ServiceRegistry', () => { const factoryA = createServiceFactory({ service: refA, deps: { b: refB }, - async factory() { - return async ({ b }) => b; - }, + factory: async ({ b }) => b, }); const factoryB = createServiceFactory({ @@ -320,15 +334,18 @@ describe('ServiceRegistry', () => { const myFactory = createServiceFactory({ service: ref1, deps: {}, - factory() { + createRootContext() { throw new Error('top-level error'); }, + factory() { + throw new Error(`error in plugin`); + }, }); const registry = new ServiceRegistry([myFactory()]); await expect(registry.get(ref1, 'catalog')).rejects.toThrow( - "Failed to instantiate service '1' because the top-level factory function threw an error, Error: top-level error", + "Failed to instantiate service '1' because createRootContext threw an error, Error: top-level error", ); }); @@ -337,9 +354,7 @@ describe('ServiceRegistry', () => { service: ref1, deps: {}, async factory() { - return () => { - throw new Error(`error in plugin`); - }; + throw new Error(`error in plugin`); }, }); diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index b4d75dac1a..68dff3f7c3 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -37,12 +37,14 @@ export class ServiceRegistry implements EnumerableServiceHolder { readonly #implementations: Map< ServiceFactory, { - factoryFunc: Promise< - (deps: { [name in string]: unknown }) => Promise - >; + context: Promise; byPlugin: Map>; } >; + readonly #rootServiceImplementations = new Map< + ServiceFactory, + Promise + >(); constructor(factories: Array>) { this.#providedFactories = new Map(factories.map(f => [f.service.id, f])); @@ -56,15 +58,13 @@ export class ServiceRegistry implements EnumerableServiceHolder { ): Promise | undefined { // Special case handling of the plugin metadata service, generating a custom factory for it each time if (ref.id === coreServices.pluginMetadata.id) { - return Promise.resolve({ + return Promise.resolve< + ServiceFactory + >({ scope: 'plugin', service: coreServices.pluginMetadata, deps: {}, - factory: async () => async () => ({ - getId() { - return pluginId; - }, - }), + factory: async () => ({ getId: () => pluginId }), }); } @@ -100,8 +100,6 @@ export class ServiceRegistry implements EnumerableServiceHolder { return Promise.resolve(resolvedFactory); } - #separateMapForTheRootService = new Map>(); - #checkForMissingDeps(factory: ServiceFactory, pluginId: string) { const missingDeps = Object.values(factory.deps).filter(ref => { if (ref.id === coreServices.pluginMetadata.id) { @@ -129,7 +127,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { get(ref: ServiceRef, pluginId: string): Promise | undefined { return this.#resolveFactory(ref, pluginId)?.then(factory => { if (factory.scope === 'root') { - let existing = this.#separateMapForTheRootService.get(factory); + let existing = this.#rootServiceImplementations.get(factory); if (!existing) { this.#checkForMissingDeps(factory, pluginId); const rootDeps = new Array>(); @@ -147,7 +145,7 @@ export class ServiceRegistry implements EnumerableServiceHolder { existing = Promise.all(rootDeps).then(entries => factory.factory(Object.fromEntries(entries)), ); - this.#separateMapForTheRootService.set(factory, existing); + this.#rootServiceImplementations.set(factory, existing); } return existing as Promise; } @@ -165,12 +163,14 @@ export class ServiceRegistry implements EnumerableServiceHolder { } implementation = { - factoryFunc: Promise.all(rootDeps) - .then(entries => factory.factory(Object.fromEntries(entries))) + context: Promise.all(rootDeps) + .then(entries => + factory.createRootContext?.(Object.fromEntries(entries)), + ) .catch(error => { const cause = stringifyError(error); throw new Error( - `Failed to instantiate service '${ref.id}' because the top-level factory function threw an error, ${cause}`, + `Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`, ); }), byPlugin: new Map(), @@ -188,10 +188,10 @@ export class ServiceRegistry implements EnumerableServiceHolder { allDeps.push(target.then(impl => [name, impl])); } - result = implementation.factoryFunc - .then(func => + result = implementation.context + .then(context => Promise.all(allDeps).then(entries => - func(Object.fromEntries(entries)), + factory.factory(Object.fromEntries(entries), context), ), ) .catch(error => {