diff --git a/.changeset/shy-boats-admire.md b/.changeset/shy-boats-admire.md new file mode 100644 index 0000000000..904fde9d1e --- /dev/null +++ b/.changeset/shy-boats-admire.md @@ -0,0 +1,5 @@ +--- +'@backstage/backend-app-api': patch +--- + +Root scoped services are now always initialized, regardless of whether they're used by any features. diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.test.ts b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts new file mode 100644 index 0000000000..8fd7441c37 --- /dev/null +++ b/packages/backend-app-api/src/wiring/BackendInitializer.test.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2022 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + createServiceRef, + createServiceFactory, +} from '@backstage/backend-plugin-api'; +import { BackendInitializer } from './BackendInitializer'; +import { ServiceRegistry } from './ServiceRegistry'; + +const rootRef = createServiceRef<{ x: number }>({ + id: '1', + scope: 'root', +}); + +const pluginRef = createServiceRef<{ x: number }>({ + id: '2', +}); + +describe('BackendInitializer', () => { + it('should initialize root scoped services', async () => { + const rootFactory = jest.fn(); + const pluginFactory = jest.fn(); + + const registry = new ServiceRegistry([ + createServiceFactory({ + service: rootRef, + deps: {}, + factory: rootFactory, + }), + createServiceFactory({ + service: pluginRef, + deps: {}, + factory: pluginFactory, + }), + ]); + + const init = new BackendInitializer(registry); + await init.start(); + + expect(rootFactory).toHaveBeenCalled(); + expect(pluginFactory).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/backend-app-api/src/wiring/BackendInitializer.ts b/packages/backend-app-api/src/wiring/BackendInitializer.ts index c86cd8382d..8311ae10bb 100644 --- a/packages/backend-app-api/src/wiring/BackendInitializer.ts +++ b/packages/backend-app-api/src/wiring/BackendInitializer.ts @@ -21,7 +21,7 @@ import { } from '@backstage/backend-plugin-api'; import { BackendRegisterInit, - ServiceHolder, + EnumerableServiceHolder, ServiceOrExtensionPoint, } from './types'; @@ -30,9 +30,9 @@ export class BackendInitializer { #features = new Map(); #registerInits = new Array(); #extensionPoints = new Map, unknown>(); - #serviceHolder: ServiceHolder; + #serviceHolder: EnumerableServiceHolder; - constructor(serviceHolder: ServiceHolder) { + constructor(serviceHolder: EnumerableServiceHolder) { this.#serviceHolder = serviceHolder; } @@ -85,6 +85,14 @@ export class BackendInitializer { } this.#started = true; + // Initialize all root scoped services + for (const ref of this.#serviceHolder.getServiceRefs()) { + if (ref.scope === 'root') { + await this.#serviceHolder.get(ref, 'root'); + } + } + + // Initialize all features for (const [feature] of this.#features) { const provides = new Set>(); diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index 9ff73f4270..208e5cf260 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -20,7 +20,7 @@ import { coreServices, } from '@backstage/backend-plugin-api'; import { stringifyError } from '@backstage/errors'; - +import { EnumerableServiceHolder } from './types'; /** * Keep in sync with `@backstage/backend-plugin-api/src/services/system/types.ts` * @internal @@ -31,7 +31,7 @@ export type InternalServiceRef = ServiceRef & { ) => Promise | (() => ServiceFactory)>; }; -export class ServiceRegistry { +export class ServiceRegistry implements EnumerableServiceHolder { readonly #providedFactories: Map; readonly #loadedDefaultFactories: Map>; readonly #implementations: Map< @@ -132,6 +132,10 @@ export class ServiceRegistry { } } + getServiceRefs(): ServiceRef[] { + return Array.from(this.#providedFactories.values()).map(f => f.service); + } + get(ref: ServiceRef, pluginId: string): Promise | undefined { return this.#resolveFactory(ref, pluginId)?.then(factory => { if (factory.scope === 'root') { diff --git a/packages/backend-app-api/src/wiring/types.ts b/packages/backend-app-api/src/wiring/types.ts index 40ce6aa1ae..76bcd5ab6d 100644 --- a/packages/backend-app-api/src/wiring/types.ts +++ b/packages/backend-app-api/src/wiring/types.ts @@ -45,9 +45,16 @@ export interface CreateSpecializedBackendOptions { services: (ServiceFactory | (() => ServiceFactory))[]; } -export type ServiceHolder = { +export interface ServiceHolder { get(api: ServiceRef, pluginId: string): Promise | undefined; -}; +} + +/** + * @internal + */ +export interface EnumerableServiceHolder extends ServiceHolder { + getServiceRefs(): ServiceRef[]; +} /** * @public