diff --git a/.changeset/silver-carrots-sort.md b/.changeset/silver-carrots-sort.md new file mode 100644 index 0000000000..fef7f7098e --- /dev/null +++ b/.changeset/silver-carrots-sort.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-plugin-api': patch +'@backstage/backend-app-api': patch +--- + +Added `initialization` option to `createServiceFactory` which defines the initialization strategy for the service. The default strategy mimics the current behavior where plugin scoped services are initialized lazily by default and root scoped services are initialized eagerly. diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts index c025d19fa7..2e48dd234b 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -30,15 +30,6 @@ import { rootLifecycleServiceFactory, } from '../services/implementations'; -const rootRef = createServiceRef<{ x: number }>({ - id: '1', - scope: 'root', -}); - -const pluginRef = createServiceRef<{ x: number }>({ - id: '2', -}); - class MockLogger { debug() {} info() {} @@ -62,33 +53,103 @@ const baseFactories = [ describe('BackendInitializer', () => { it('should initialize root scoped services', async () => { - const rootFactory = jest.fn(); - const pluginFactory = jest.fn(); + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + scope: 'root', + }); + const ref2 = createServiceRef<{ x: number }>({ + id: '2', + scope: 'root', + }); + const ref3 = createServiceRef<{ x: number }>({ + id: '3', + scope: 'root', + }); + const factory1 = jest.fn(); + const factory2 = jest.fn(); + const factory3 = jest.fn(); const services = [ + ...baseFactories, createServiceFactory({ - service: rootRef, + service: ref1, + initialization: 'always', deps: {}, - factory: rootFactory, + factory: factory1, })(), createServiceFactory({ - service: pluginRef, + service: ref2, deps: {}, - factory: pluginFactory, + factory: factory2, })(), - rootLifecycleServiceFactory(), createServiceFactory({ - service: coreServices.rootLogger, + service: ref3, + initialization: 'lazy', deps: {}, - factory: () => new MockLogger(), + factory: factory3, })(), ]; const init = new BackendInitializer(services); await init.start(); - expect(rootFactory).toHaveBeenCalled(); - expect(pluginFactory).not.toHaveBeenCalled(); + expect(factory1).toHaveBeenCalled(); + expect(factory2).toHaveBeenCalled(); + expect(factory3).not.toHaveBeenCalled(); + }); + + it('should initialize plugin scoped services with eager initialization', async () => { + const ref1 = createServiceRef<{ x: number }>({ + id: '1', + }); + const ref2 = createServiceRef<{ x: number }>({ + id: '2', + }); + const ref3 = createServiceRef<{ x: number }>({ + id: '3', + }); + const factory1 = jest.fn(); + const factory2 = jest.fn(); + const factory3 = jest.fn(); + + const services = [ + ...baseFactories, + createServiceFactory({ + service: ref1, + initialization: 'always', + deps: {}, + factory: factory1, + })(), + createServiceFactory({ + service: ref2, + deps: {}, + factory: factory2, + })(), + createServiceFactory({ + service: ref3, + initialization: 'lazy', + deps: {}, + factory: factory3, + })(), + ]; + + const init = new BackendInitializer(services); + init.add( + createBackendPlugin({ + pluginId: 'test', + register(reg) { + reg.registerInit({ + deps: {}, + async init() {}, + }); + }, + })(), + ); + await init.start(); + + expect(factory1).toHaveBeenCalled(); + expect(factory2).not.toHaveBeenCalled(); + expect(factory3).not.toHaveBeenCalled(); }); it('should initialize modules with extension points', async () => { diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index 2da3a5a226..f4b4fde804 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -170,11 +170,7 @@ export class BackendInitializer { } // Initialize all root scoped services - for (const ref of this.#serviceRegistry.getServiceRefs()) { - if (ref.scope === 'root') { - await this.#serviceRegistry.get(ref, 'root'); - } - } + await this.#serviceRegistry.initializeEagerServicesWithScope('root'); const pluginInits = new Map(); const moduleInits = new Map>(); @@ -235,6 +231,12 @@ export class BackendInitializer { // All plugins are initialized in parallel await Promise.all( allPluginIds.map(async pluginId => { + // Initialize all eager services + await this.#serviceRegistry.initializeEagerServicesWithScope( + 'plugin', + pluginId, + ); + // Modules are initialized before plugins, so that they can provide extension to the plugin const modules = moduleInits.get(pluginId); if (modules) { diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index ad52e645bb..6e20dd98e8 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -199,8 +199,20 @@ export class ServiceRegistry { this.#providedFactories.set(factoryId, toInternalServiceFactory(factory)); } - getServiceRefs(): ServiceRef[] { - return Array.from(this.#providedFactories.values()).map(f => f.service); + async initializeEagerServicesWithScope( + scope: 'root' | 'plugin', + pluginId: string = 'root', + ) { + for (const factory of this.#providedFactories.values()) { + if (factory.service.scope === scope) { + // Root-scoped services are eager by default, plugin-scoped are lazy by default + if (scope === 'root' && factory.initialization !== 'lazy') { + await this.get(factory.service, pluginId); + } else if (scope === 'plugin' && factory.initialization === 'always') { + await this.get(factory.service, pluginId); + } + } + } } get(ref: ServiceRef, pluginId: string): Promise | undefined { diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index 329502369e..9e751cbd39 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -63,6 +63,7 @@ export interface InternalServiceFactory< TScope extends 'plugin' | 'root' = 'plugin' | 'root', > extends ServiceFactory { version: 'v1'; + initialization?: 'always' | 'lazy'; deps: { [key in string]: ServiceRef }; createRootContext?(deps: { [key in string]: unknown }): Promise; factory( @@ -140,6 +141,7 @@ export interface RootServiceFactoryConfig< TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, > { + initialization?: 'always' | 'lazy'; service: ServiceRef; deps: TDeps; factory(deps: ServiceRefsToInstances): TImpl | Promise; @@ -152,6 +154,7 @@ export interface PluginServiceFactoryConfig< TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, > { + initialization?: 'always' | 'lazy'; service: ServiceRef; deps: TDeps; createRootContext?( @@ -251,6 +254,7 @@ export function createServiceFactory< $$type: '@backstage/BackendFeature', version: 'v1', service: c.service, + initialization: c.initialization, deps: c.deps, factory: async (deps: TDeps) => c.factory(deps), }; @@ -265,6 +269,7 @@ export function createServiceFactory< $$type: '@backstage/BackendFeature', version: 'v1', service: c.service, + initialization: c.initialization, ...('createRootContext' in c ? { createRootContext: async (deps: TDeps) =>