backend-app-api: add initialization option to createServiceFactory

Co-authored-by: Patrik Oldsberg <poldsberg@gmail.com>
Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2024-04-02 14:18:06 +02:00
parent 516e1e3fbc
commit 54f2ac8c59
5 changed files with 113 additions and 27 deletions
+6
View File
@@ -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.
@@ -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 () => {
@@ -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<string, BackendRegisterInit>();
const moduleInits = new Map<string, Map<string, BackendRegisterInit>>();
@@ -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) {
@@ -199,8 +199,20 @@ export class ServiceRegistry {
this.#providedFactories.set(factoryId, toInternalServiceFactory(factory));
}
getServiceRefs(): ServiceRef<unknown>[] {
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<T>(ref: ServiceRef<T>, pluginId: string): Promise<T> | undefined {
@@ -63,6 +63,7 @@ export interface InternalServiceFactory<
TScope extends 'plugin' | 'root' = 'plugin' | 'root',
> extends ServiceFactory<TService, TScope> {
version: 'v1';
initialization?: 'always' | 'lazy';
deps: { [key in string]: ServiceRef<unknown> };
createRootContext?(deps: { [key in string]: unknown }): Promise<unknown>;
factory(
@@ -140,6 +141,7 @@ export interface RootServiceFactoryConfig<
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
> {
initialization?: 'always' | 'lazy';
service: ServiceRef<TService, 'root'>;
deps: TDeps;
factory(deps: ServiceRefsToInstances<TDeps, 'root'>): TImpl | Promise<TImpl>;
@@ -152,6 +154,7 @@ export interface PluginServiceFactoryConfig<
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
> {
initialization?: 'always' | 'lazy';
service: ServiceRef<TService, 'plugin'>;
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) =>