From 11aaaa496a97a740ef52f080cc06e82b3625752b Mon Sep 17 00:00:00 2001 From: Camila Belo Date: Mon, 8 Jul 2024 16:08:04 +0200 Subject: [PATCH] feat: start implementing multiple service factories Co-authored-by: Patrik Oldsberg Signed-off-by: Camila Belo --- packages/backend-app-api/api-report-alpha.md | 1 + packages/backend-app-api/api-report.md | 21 +- .../src/wiring/ServiceRegistry.ts | 265 +++++++++++------- packages/backend-defaults/api-report-auth.md | 1 + packages/backend-defaults/api-report-cache.md | 1 + .../backend-defaults/api-report-database.md | 1 + .../backend-defaults/api-report-discovery.md | 1 + .../backend-defaults/api-report-httpAuth.md | 1 + .../backend-defaults/api-report-httpRouter.md | 1 + .../backend-defaults/api-report-lifecycle.md | 1 + .../backend-defaults/api-report-logger.md | 1 + .../api-report-permissions.md | 1 + .../backend-defaults/api-report-rootConfig.md | 4 +- .../backend-defaults/api-report-rootHealth.md | 1 + .../api-report-rootHttpRouter.md | 4 +- .../api-report-rootLifecycle.md | 1 + .../backend-defaults/api-report-rootLogger.md | 1 + .../backend-defaults/api-report-scheduler.md | 1 + .../backend-defaults/api-report-urlReader.md | 1 + .../backend-defaults/api-report-userInfo.md | 1 + .../urlReader/urlReaderServiceFactory.ts | 29 +- .../api-report.md | 7 +- .../backend-plugin-api/api-report-alpha.md | 3 +- packages/backend-plugin-api/api-report.md | 125 ++++++--- packages/backend-plugin-api/src/deprecated.ts | 9 +- .../src/services/system/types.ts | 149 +++++++--- packages/backend-test-utils/api-report.md | 123 +++++--- .../src/next/wiring/ServiceFactoryTester.ts | 37 ++- plugins/catalog-node/api-report-alpha.md | 2 +- plugins/events-node/api-report.md | 3 +- plugins/notifications-node/api-report.md | 6 +- .../search-backend-node/api-report-alpha.md | 6 +- plugins/signals-node/api-report.md | 4 +- 33 files changed, 563 insertions(+), 250 deletions(-) diff --git a/packages/backend-app-api/api-report-alpha.md b/packages/backend-app-api/api-report-alpha.md index e4769f4e72..b811208845 100644 --- a/packages/backend-app-api/api-report-alpha.md +++ b/packages/backend-app-api/api-report-alpha.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const featureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, 'root', + true, undefined >; diff --git a/packages/backend-app-api/api-report.md b/packages/backend-app-api/api-report.md index 7f9b54dd8f..4019ad3086 100644 --- a/packages/backend-app-api/api-report.md +++ b/packages/backend-app-api/api-report.md @@ -49,6 +49,7 @@ import { UserInfoService } from '@backstage/backend-plugin-api'; export const authServiceFactory: ServiceFactoryCompat< AuthService, 'plugin', + true, undefined >; @@ -80,6 +81,7 @@ export interface Backend { export const cacheServiceFactory: ServiceFactoryCompat< CacheService, 'plugin', + true, undefined >; @@ -113,6 +115,7 @@ export interface CreateSpecializedBackendOptions { export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, 'plugin', + true, undefined >; @@ -135,6 +138,7 @@ export type DefaultRootHttpRouterOptions = DefaultRootHttpRouterOptions_2; export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, 'plugin', + true, undefined >; @@ -161,6 +165,7 @@ export class HostDiscovery implements DiscoveryService { export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, 'plugin', + true, undefined >; @@ -168,6 +173,7 @@ export const httpAuthServiceFactory: ServiceFactoryCompat< export const httpRouterServiceFactory: ServiceFactoryCompat< HttpRouterService, 'plugin', + true, undefined >; @@ -191,6 +197,7 @@ export type IdentityFactoryOptions = { export const identityServiceFactory: ServiceFactoryCompat< IdentityService, 'plugin', + true, IdentityFactoryOptions >; @@ -203,6 +210,7 @@ export type LifecycleMiddlewareOptions = LifecycleMiddlewareOptions_2; export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, 'plugin', + true, undefined >; @@ -220,6 +228,7 @@ export function loadBackendConfig(options: { export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, 'plugin', + true, undefined >; @@ -248,6 +257,7 @@ export type MiddlewareFactoryOptions = MiddlewareFactoryOptions_2; export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, 'plugin', + true, undefined >; @@ -278,6 +288,7 @@ export interface RootConfigFactoryOptions { export const rootConfigServiceFactory: ServiceFactoryCompat< RootConfigService, 'root', + true, RootConfigFactoryOptions >; @@ -294,13 +305,14 @@ export type RootHttpRouterFactoryOptions = RootHttpRouterFactoryOptions_2; // @public @deprecated (undocumented) export const rootHttpRouterServiceFactory: (( options?: RootHttpRouterFactoryOptions_2 | undefined, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // @public @deprecated export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, 'root', + true, undefined >; @@ -308,6 +320,7 @@ export const rootLifecycleServiceFactory: ServiceFactoryCompat< export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', + true, undefined >; @@ -315,6 +328,7 @@ export const rootLoggerServiceFactory: ServiceFactoryCompat< export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, 'plugin', + true, undefined >; @@ -322,6 +336,7 @@ export const schedulerServiceFactory: ServiceFactoryCompat< export const tokenManagerServiceFactory: ServiceFactoryCompat< TokenManagerService, 'plugin', + true, undefined >; @@ -329,6 +344,7 @@ export const tokenManagerServiceFactory: ServiceFactoryCompat< export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, 'plugin', + true, undefined >; @@ -336,6 +352,7 @@ export const urlReaderServiceFactory: ServiceFactoryCompat< export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, 'plugin', + true, undefined >; diff --git a/packages/backend-app-api/src/wiring/ServiceRegistry.ts b/packages/backend-app-api/src/wiring/ServiceRegistry.ts index 56d8d611bb..e1644dad89 100644 --- a/packages/backend-app-api/src/wiring/ServiceRegistry.ts +++ b/packages/backend-app-api/src/wiring/ServiceRegistry.ts @@ -58,12 +58,24 @@ function createPluginMetadataServiceFactory(pluginId: string) { export class ServiceRegistry { static create(factories: Array): ServiceRegistry { - const registry = new ServiceRegistry(factories); + const factoryMap = new Map(); + for (const factory of factories) { + if (factory.service.singleton) { + factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]); + } else { + const existing = factoryMap.get(factory.service.id) ?? []; + factoryMap.set( + factory.service.id, + existing.concat(toInternalServiceFactory(factory)), + ); + } + } + const registry = new ServiceRegistry(factoryMap); registry.checkForCircularDeps(); return registry; } - readonly #providedFactories: Map; + readonly #providedFactories: Map; readonly #loadedDefaultFactories: Map< Function, Promise @@ -82,10 +94,8 @@ export class ServiceRegistry { readonly #addedFactoryIds = new Set(); readonly #instantiatedFactories = new Set(); - private constructor(factories: Array) { - this.#providedFactories = new Map( - factories.map(sf => [sf.service.id, toInternalServiceFactory(sf)]), - ); + private constructor(factories: Map) { + this.#providedFactories = factories; this.#loadedDefaultFactories = new Map(); this.#implementations = new Map(); } @@ -93,17 +103,17 @@ export class ServiceRegistry { #resolveFactory( ref: ServiceRef, pluginId: string, - ): Promise | undefined { + ): 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([ toInternalServiceFactory(createPluginMetadataServiceFactory(pluginId)), - ); + ]); } let resolvedFactory: - | Promise - | InternalServiceFactory + | Promise + | InternalServiceFactory[] | undefined = this.#providedFactories.get(ref.id); const { __defaultFactory: defaultFactory } = ref as InternalServiceRef; if (!resolvedFactory && !defaultFactory) { @@ -120,15 +130,18 @@ export class ServiceRegistry { ); this.#loadedDefaultFactories.set(defaultFactory!, loadedFactory); } - resolvedFactory = loadedFactory.catch(error => { - throw new Error( - `Failed to instantiate service '${ - ref.id - }' because the default factory loader threw an error, ${stringifyError( - error, - )}`, - ); - }); + resolvedFactory = loadedFactory.then( + factory => [factory], + error => { + throw new Error( + `Failed to instantiate service '${ + ref.id + }' because the default factory loader threw an error, ${stringifyError( + error, + )}`, + ); + }, + ); } return Promise.resolve(resolvedFactory); @@ -142,6 +155,9 @@ export class ServiceRegistry { if (this.#providedFactories.get(ref.id)) { return false; } + if (!ref.singleton) { + return false; + } return !(ref as InternalServiceRef).__defaultFactory; }); @@ -156,13 +172,13 @@ export class ServiceRegistry { checkForCircularDeps(): void { const graph = DependencyGraph.fromIterable( - Array.from(this.#providedFactories).map( - ([serviceId, serviceFactory]) => ({ - value: serviceId, - provides: [serviceId], - consumes: Object.values(serviceFactory.deps).map(d => d.id), - }), - ), + Array.from(this.#providedFactories).map(([serviceId, factories]) => ({ + value: serviceId, + provides: [serviceId], + consumes: factories.flatMap(factory => + Object.values(factory.deps).map(d => d.id), + ), + })), ); const circularDependencies = Array.from(graph.detectCircularDependencies()); @@ -183,27 +199,36 @@ export class ServiceRegistry { ); } - if (this.#addedFactoryIds.has(factoryId)) { - throw new Error( - `Duplicate service implementations provided for ${factoryId}`, - ); - } - if (this.#instantiatedFactories.has(factoryId)) { throw new Error( `Unable to set service factory with id ${factoryId}, service has already been instantiated`, ); } - this.#addedFactoryIds.add(factoryId); - this.#providedFactories.set(factoryId, toInternalServiceFactory(factory)); + if (factory.service.singleton) { + if (this.#addedFactoryIds.has(factoryId)) { + throw new Error( + `Duplicate service implementations provided for ${factoryId}`, + ); + } + + this.#addedFactoryIds.add(factoryId); + this.#providedFactories.set(factoryId, [ + toInternalServiceFactory(factory), + ]); + } else { + const newFactories = ( + this.#providedFactories.get(factoryId) ?? [] + ).concat(toInternalServiceFactory(factory)); + this.#providedFactories.set(factoryId, newFactories); + } } async initializeEagerServicesWithScope( scope: 'root' | 'plugin', pluginId: string = 'root', ) { - for (const factory of this.#providedFactories.values()) { + 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') { @@ -215,88 +240,112 @@ export class ServiceRegistry { } } - get(ref: ServiceRef, pluginId: string): Promise | undefined { + get( + ref: ServiceRef, + pluginId: string, + ): Promise | undefined { this.#instantiatedFactories.add(ref.id); - return this.#resolveFactory(ref, pluginId)?.then(factory => { - if (factory.service.scope === 'root') { - let existing = this.#rootServiceImplementations.get(factory); - if (!existing) { - this.#checkForMissingDeps(factory, pluginId); - const rootDeps = new Array>(); + const resolvedFactory = this.#resolveFactory(ref, pluginId); - for (const [name, serviceRef] of Object.entries(factory.deps)) { - if (serviceRef.scope !== 'root') { - throw new Error( - `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`, - ); + if (!resolvedFactory) { + return ref.singleton + ? undefined + : (Promise.resolve([]) as + | Promise + | undefined); + } + + return resolvedFactory + .then(factories => { + return Promise.all( + factories.map(factory => { + if (factory.service.scope === 'root') { + let existing = this.#rootServiceImplementations.get(factory); + if (!existing) { + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); + + for (const [name, serviceRef] of Object.entries(factory.deps)) { + if (serviceRef.scope !== 'root') { + throw new Error( + `Failed to instantiate 'root' scoped service '${ref.id}' because it depends on '${serviceRef.scope}' scoped service '${serviceRef.id}'.`, + ); + } + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); + } + + existing = Promise.all(rootDeps).then(entries => + factory.factory(Object.fromEntries(entries), undefined), + ); + this.#rootServiceImplementations.set(factory, existing); + } + return existing as Promise; } - const target = this.get(serviceRef, pluginId)!; - rootDeps.push(target.then(impl => [name, impl])); - } - existing = Promise.all(rootDeps).then(entries => - factory.factory(Object.fromEntries(entries), undefined), - ); - this.#rootServiceImplementations.set(factory, existing); - } - return existing as Promise; - } + let implementation = this.#implementations.get(factory); + if (!implementation) { + this.#checkForMissingDeps(factory, pluginId); + const rootDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); - let implementation = this.#implementations.get(factory); - if (!implementation) { - this.#checkForMissingDeps(factory, pluginId); - const rootDeps = new Array>(); + for (const [name, serviceRef] of Object.entries(factory.deps)) { + if (serviceRef.scope === 'root') { + const target = this.get(serviceRef, pluginId)!; + rootDeps.push(target.then(impl => [name, impl])); + } + } - for (const [name, serviceRef] of Object.entries(factory.deps)) { - if (serviceRef.scope === 'root') { - const target = this.get(serviceRef, pluginId)!; - rootDeps.push(target.then(impl => [name, impl])); - } - } + implementation = { + 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 createRootContext threw an error, ${cause}`, + ); + }), + byPlugin: new Map(), + }; - implementation = { - 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 createRootContext threw an error, ${cause}`, - ); - }), - byPlugin: new Map(), - }; + this.#implementations.set(factory, implementation); + } - this.#implementations.set(factory, implementation); - } + let result = implementation.byPlugin.get(pluginId) as Promise; + if (!result) { + const allDeps = new Array< + Promise<[name: string, impl: unknown]> + >(); - let result = implementation.byPlugin.get(pluginId) as Promise; - if (!result) { - const allDeps = new Array>(); + for (const [name, serviceRef] of Object.entries(factory.deps)) { + const target = this.get(serviceRef, pluginId)!; + allDeps.push(target.then(impl => [name, impl])); + } - for (const [name, serviceRef] of Object.entries(factory.deps)) { - const target = this.get(serviceRef, pluginId)!; - allDeps.push(target.then(impl => [name, impl])); - } - - result = implementation.context - .then(context => - Promise.all(allDeps).then(entries => - factory.factory(Object.fromEntries(entries), context), - ), - ) - .catch(error => { - const cause = stringifyError(error); - throw new Error( - `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`, - ); - }); - implementation.byPlugin.set(pluginId, result); - } - - return result; - }); + result = implementation.context + .then(context => + Promise.all(allDeps).then(entries => + factory.factory(Object.fromEntries(entries), context), + ), + ) + .catch(error => { + const cause = stringifyError(error); + throw new Error( + `Failed to instantiate service '${ref.id}' for '${pluginId}' because the factory function threw an error, ${cause}`, + ); + }); + implementation.byPlugin.set(pluginId, result); + } + return result; + }), + ); + }) + .then(results => (ref.singleton ? results[0] : results)); } } diff --git a/packages/backend-defaults/api-report-auth.md b/packages/backend-defaults/api-report-auth.md index 7df0b394e3..85287c524e 100644 --- a/packages/backend-defaults/api-report-auth.md +++ b/packages/backend-defaults/api-report-auth.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const authServiceFactory: ServiceFactoryCompat< AuthService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-cache.md b/packages/backend-defaults/api-report-cache.md index 0b2d8706df..c6debd6a2b 100644 --- a/packages/backend-defaults/api-report-cache.md +++ b/packages/backend-defaults/api-report-cache.md @@ -28,6 +28,7 @@ export type CacheManagerOptions = { export const cacheServiceFactory: ServiceFactoryCompat< CacheService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-database.md b/packages/backend-defaults/api-report-database.md index 0d54b43777..d7585cc791 100644 --- a/packages/backend-defaults/api-report-database.md +++ b/packages/backend-defaults/api-report-database.md @@ -35,6 +35,7 @@ export type DatabaseManagerOptions = { export const databaseServiceFactory: ServiceFactoryCompat< DatabaseService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-discovery.md b/packages/backend-defaults/api-report-discovery.md index dbb469f758..dc0d9b2bbe 100644 --- a/packages/backend-defaults/api-report-discovery.md +++ b/packages/backend-defaults/api-report-discovery.md @@ -11,6 +11,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const discoveryServiceFactory: ServiceFactoryCompat< DiscoveryService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-httpAuth.md b/packages/backend-defaults/api-report-httpAuth.md index 613e6a4452..d8d283c01e 100644 --- a/packages/backend-defaults/api-report-httpAuth.md +++ b/packages/backend-defaults/api-report-httpAuth.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const httpAuthServiceFactory: ServiceFactoryCompat< HttpAuthService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-httpRouter.md b/packages/backend-defaults/api-report-httpRouter.md index a2bf1cd39b..7673367f18 100644 --- a/packages/backend-defaults/api-report-httpRouter.md +++ b/packages/backend-defaults/api-report-httpRouter.md @@ -18,6 +18,7 @@ export function createLifecycleMiddleware( export const httpRouterServiceFactory: ServiceFactoryCompat< HttpRouterService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-lifecycle.md b/packages/backend-defaults/api-report-lifecycle.md index 36adbbf6e7..ad41fd4fcb 100644 --- a/packages/backend-defaults/api-report-lifecycle.md +++ b/packages/backend-defaults/api-report-lifecycle.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const lifecycleServiceFactory: ServiceFactoryCompat< LifecycleService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-logger.md b/packages/backend-defaults/api-report-logger.md index c66f886bf4..c881d20344 100644 --- a/packages/backend-defaults/api-report-logger.md +++ b/packages/backend-defaults/api-report-logger.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const loggerServiceFactory: ServiceFactoryCompat< LoggerService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-permissions.md b/packages/backend-defaults/api-report-permissions.md index 86b9605606..92e3feaeaf 100644 --- a/packages/backend-defaults/api-report-permissions.md +++ b/packages/backend-defaults/api-report-permissions.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const permissionsServiceFactory: ServiceFactoryCompat< PermissionsService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-rootConfig.md b/packages/backend-defaults/api-report-rootConfig.md index 61f5c9dc9d..60fed5734b 100644 --- a/packages/backend-defaults/api-report-rootConfig.md +++ b/packages/backend-defaults/api-report-rootConfig.md @@ -28,8 +28,8 @@ export interface RootConfigFactoryOptions { // @public (undocumented) export const rootConfigServiceFactory: (( options?: RootConfigFactoryOptions, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootHealth.md b/packages/backend-defaults/api-report-rootHealth.md index c69f66e747..ca104ecf37 100644 --- a/packages/backend-defaults/api-report-rootHealth.md +++ b/packages/backend-defaults/api-report-rootHealth.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const rootHealthServiceFactory: ServiceFactoryCompat< RootHealthService, 'root', + true, undefined >; diff --git a/packages/backend-defaults/api-report-rootHttpRouter.md b/packages/backend-defaults/api-report-rootHttpRouter.md index 329398a7b8..a6e5a31428 100644 --- a/packages/backend-defaults/api-report-rootHttpRouter.md +++ b/packages/backend-defaults/api-report-rootHttpRouter.md @@ -143,8 +143,8 @@ export type RootHttpRouterFactoryOptions = { // @public (undocumented) export const rootHttpRouterServiceFactory: (( options?: RootHttpRouterFactoryOptions, -) => ServiceFactory) & - ServiceFactory; +) => ServiceFactory) & + ServiceFactory; // (No @packageDocumentation comment for this package) ``` diff --git a/packages/backend-defaults/api-report-rootLifecycle.md b/packages/backend-defaults/api-report-rootLifecycle.md index faa83706a7..52eaa3fe0e 100644 --- a/packages/backend-defaults/api-report-rootLifecycle.md +++ b/packages/backend-defaults/api-report-rootLifecycle.md @@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api'; export const rootLifecycleServiceFactory: ServiceFactoryCompat< RootLifecycleService, 'root', + true, undefined >; diff --git a/packages/backend-defaults/api-report-rootLogger.md b/packages/backend-defaults/api-report-rootLogger.md index 075a8b4aa9..d91aaf0ae8 100644 --- a/packages/backend-defaults/api-report-rootLogger.md +++ b/packages/backend-defaults/api-report-rootLogger.md @@ -14,6 +14,7 @@ import { transport } from 'winston'; export const rootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', + true, undefined >; diff --git a/packages/backend-defaults/api-report-scheduler.md b/packages/backend-defaults/api-report-scheduler.md index a3467b97de..7d8706def2 100644 --- a/packages/backend-defaults/api-report-scheduler.md +++ b/packages/backend-defaults/api-report-scheduler.md @@ -21,6 +21,7 @@ export class DefaultSchedulerService { export const schedulerServiceFactory: ServiceFactoryCompat< SchedulerService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-urlReader.md b/packages/backend-defaults/api-report-urlReader.md index 0980e3cd67..431e2af701 100644 --- a/packages/backend-defaults/api-report-urlReader.md +++ b/packages/backend-defaults/api-report-urlReader.md @@ -429,6 +429,7 @@ export class UrlReaders { export const urlReaderServiceFactory: ServiceFactoryCompat< UrlReaderService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/api-report-userInfo.md b/packages/backend-defaults/api-report-userInfo.md index b77878ff34..31ae1dd8bb 100644 --- a/packages/backend-defaults/api-report-userInfo.md +++ b/packages/backend-defaults/api-report-userInfo.md @@ -10,6 +10,7 @@ import { UserInfoService } from '@backstage/backend-plugin-api'; export const userInfoServiceFactory: ServiceFactoryCompat< UserInfoService, 'plugin', + true, undefined >; diff --git a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts index 810882edf8..9859166ba0 100644 --- a/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts +++ b/packages/backend-defaults/src/entrypoints/urlReader/urlReaderServiceFactory.ts @@ -14,12 +14,37 @@ * limitations under the License. */ +import { ReaderFactory } from './lib'; import { UrlReaders } from './lib/UrlReaders'; import { coreServices, createServiceFactory, + createServiceRef, } from '@backstage/backend-plugin-api'; +/** + * @public + * A non-singleton reference to URL Reader factory services. + * + * @example + * Creating a service factory implementation for a Custom URL Reader. + * ```ts + * createServiceFactory({ + * service: urlReaderProviderFactoriesServiceRef, + * deps: {}, + * async factory() { + * return CustomUrlReader.factory; + * }, + * }); + * ``` + */ +export const urlReaderProviderFactoriesServiceRef = + createServiceRef({ + id: 'core.urlReader.factories', + scope: 'plugin', + singleton: false, + }); + /** * Reading content from external systems. * @@ -34,11 +59,13 @@ export const urlReaderServiceFactory = createServiceFactory({ deps: { config: coreServices.rootConfig, logger: coreServices.logger, + factories: urlReaderProviderFactoriesServiceRef, }, - async factory({ config, logger }) { + async factory({ config, logger, factories }) { return UrlReaders.default({ config, logger, + factories, }); }, }); diff --git a/packages/backend-dynamic-feature-service/api-report.md b/packages/backend-dynamic-feature-service/api-report.md index 39aa60b09b..b74bbcdc95 100644 --- a/packages/backend-dynamic-feature-service/api-report.md +++ b/packages/backend-dynamic-feature-service/api-report.md @@ -117,6 +117,7 @@ export interface DynamicPluginsFactoryOptions { export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactoryCompat< FeatureDiscoveryService, 'root', + true, undefined >; @@ -127,6 +128,7 @@ export const dynamicPluginsFrontendSchemas: BackendFeatureCompat; export const dynamicPluginsRootLoggerServiceFactory: ServiceFactoryCompat< RootLoggerService, 'root', + true, undefined >; @@ -147,6 +149,7 @@ export interface DynamicPluginsSchemasService { export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat< DynamicPluginsSchemasService, 'root', + true, DynamicPluginsSchemasOptions >; @@ -154,13 +157,15 @@ export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat< export const dynamicPluginsServiceFactory: ServiceFactoryCompat< DynamicPluginProvider, 'root', + true, DynamicPluginsFactoryOptions >; // @public (undocumented) export const dynamicPluginsServiceRef: ServiceRef< DynamicPluginProvider, - 'root' + 'root', + true >; // @public (undocumented) diff --git a/packages/backend-plugin-api/api-report-alpha.md b/packages/backend-plugin-api/api-report-alpha.md index 81b378a671..9a82d3adbe 100644 --- a/packages/backend-plugin-api/api-report-alpha.md +++ b/packages/backend-plugin-api/api-report-alpha.md @@ -17,7 +17,8 @@ export interface FeatureDiscoveryService { // @alpha export const featureDiscoveryServiceRef: ServiceRef< FeatureDiscoveryService, - 'root' + 'root', + true >; // (No @packageDocumentation comment for this package) diff --git a/packages/backend-plugin-api/api-report.md b/packages/backend-plugin-api/api-report.md index b3c35d3699..3925d3b2f4 100644 --- a/packages/backend-plugin-api/api-report.md +++ b/packages/backend-plugin-api/api-report.md @@ -189,28 +189,28 @@ export type CacheServiceSetOptions = { // @public export namespace coreServices { - const auth: ServiceRef; - const userInfo: ServiceRef; - const cache: ServiceRef; - const rootConfig: ServiceRef; - const database: ServiceRef; - const discovery: ServiceRef; - const rootHealth: ServiceRef; - const httpAuth: ServiceRef; - const httpRouter: ServiceRef; - const lifecycle: ServiceRef; - const logger: ServiceRef; - const permissions: ServiceRef; - const pluginMetadata: ServiceRef; - const rootHttpRouter: ServiceRef; - const rootLifecycle: ServiceRef; - const rootLogger: ServiceRef; - const scheduler: ServiceRef; + const auth: ServiceRef; + const userInfo: ServiceRef; + const cache: ServiceRef; + const rootConfig: ServiceRef; + const database: ServiceRef; + const discovery: ServiceRef; + const rootHealth: ServiceRef; + const httpAuth: ServiceRef; + const httpRouter: ServiceRef; + const lifecycle: ServiceRef; + const logger: ServiceRef; + const permissions: ServiceRef; + const pluginMetadata: ServiceRef; + const rootHttpRouter: ServiceRef; + const rootLifecycle: ServiceRef; + const rootLogger: ServiceRef; + const scheduler: ServiceRef; const // @deprecated - tokenManager: ServiceRef; - const urlReader: ServiceRef; + tokenManager: ServiceRef; + const urlReader: ServiceRef; const // @deprecated - identity: ServiceRef; + identity: ServiceRef; } // @public @@ -251,18 +251,20 @@ export interface CreateExtensionPointOptions { // @public export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, TOpts extends object | undefined = undefined, >( - options: RootServiceFactoryOptions, -): ServiceFactoryCompat; + options: RootServiceFactoryOptions, +): ServiceFactoryCompat; // @public @deprecated export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -271,12 +273,13 @@ export function createServiceFactory< >( options: ( options?: TOpts, - ) => RootServiceFactoryOptions, -): ServiceFactoryCompat; + ) => RootServiceFactoryOptions, +): ServiceFactoryCompat; // @public export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -284,12 +287,19 @@ export function createServiceFactory< TContext = undefined, TOpts extends object | undefined = undefined, >( - options: PluginServiceFactoryOptions, -): ServiceFactoryCompat; + options: PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; // @public @deprecated export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -299,18 +309,34 @@ export function createServiceFactory< >( options: ( options?: TOpts, - ) => PluginServiceFactoryOptions, -): ServiceFactoryCompat; + ) => PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; // @public export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; // @public export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; + +// @public +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; + +// @public +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; // @public export interface DatabaseService { @@ -447,16 +473,18 @@ export interface PluginMetadataService { // @public @deprecated (undocumented) export type PluginServiceFactoryConfig< TService, + TSingleton extends boolean, TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, -> = PluginServiceFactoryOptions; +> = PluginServiceFactoryOptions; // @public (undocumented) export interface PluginServiceFactoryOptions< TService, + TSingleton extends boolean, TContext, TImpl extends TService, TDeps extends { @@ -476,7 +504,7 @@ export interface PluginServiceFactoryOptions< ): TImpl | Promise; initialization?: 'always' | 'lazy'; // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @@ -538,15 +566,17 @@ export interface RootLoggerService extends LoggerService {} // @public @deprecated (undocumented) export type RootServiceFactoryConfig< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; }, -> = RootServiceFactoryOptions; +> = RootServiceFactoryOptions; // @public (undocumented) export interface RootServiceFactoryOptions< - TService, + TService, // TODO(Rugvip): Can we forward the entire service ref type here instead of forwarding each type arg once the callback form is gone? + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef; @@ -558,7 +588,7 @@ export interface RootServiceFactoryOptions< factory(deps: ServiceRefsToInstances): TImpl | Promise; initialization?: 'always' | 'lazy'; // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @@ -639,21 +669,23 @@ export type SearchResponseFile = UrlReaderServiceSearchResponseFile; export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TSingleton extends boolean = boolean, > extends BackendFeature { // (undocumented) - service: ServiceRef; + service: ServiceRef; } // @public @deprecated (undocumented) export interface ServiceFactoryCompat< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TSingleton extends boolean = boolean, TOpts extends object | undefined = undefined, -> extends ServiceFactory { +> extends ServiceFactory { // @deprecated (undocumented) ( ...options: undefined extends TOpts ? [] : [options?: TOpts] - ): ServiceFactory; + ): ServiceFactory; } // @public @deprecated @@ -663,9 +695,11 @@ export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); export type ServiceRef< TService, TScope extends 'root' | 'plugin' = 'root' | 'plugin', + TSingleton extends boolean = boolean, > = { id: string; scope: TScope; + singleton: TSingleton; T: TService; $$type: '@backstage/ServiceRef'; }; @@ -674,10 +708,15 @@ export type ServiceRef< export type ServiceRefConfig< TService, TScope extends 'root' | 'plugin', -> = ServiceRefOptions; + TSingleton extends boolean, +> = ServiceRefOptions; // @public (undocumented) -export interface ServiceRefOptions { +export interface ServiceRefOptions< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean, +> { // (undocumented) defaultFactory?( service: ServiceRef, @@ -690,6 +729,8 @@ export interface ServiceRefOptions { id: string; // (undocumented) scope?: TScope; + // (undocumented) + singleton?: TSingleton; } // @public @deprecated diff --git a/packages/backend-plugin-api/src/deprecated.ts b/packages/backend-plugin-api/src/deprecated.ts index 17ccccf320..e68affa0fa 100644 --- a/packages/backend-plugin-api/src/deprecated.ts +++ b/packages/backend-plugin-api/src/deprecated.ts @@ -28,7 +28,8 @@ import { export type ServiceRefConfig< TService, TScope extends 'root' | 'plugin', -> = ServiceRefOptions; + TSingleton extends boolean, +> = ServiceRefOptions; /** * @public @@ -36,9 +37,10 @@ export type ServiceRefConfig< */ export type RootServiceFactoryConfig< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, -> = RootServiceFactoryOptions; +> = RootServiceFactoryOptions; /** * @public @@ -46,7 +48,8 @@ export type RootServiceFactoryConfig< */ export type PluginServiceFactoryConfig< TService, + TSingleton extends boolean, TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, -> = PluginServiceFactoryOptions; +> = PluginServiceFactoryOptions; diff --git a/packages/backend-plugin-api/src/services/system/types.ts b/packages/backend-plugin-api/src/services/system/types.ts index d31350806d..e856136ddc 100644 --- a/packages/backend-plugin-api/src/services/system/types.ts +++ b/packages/backend-plugin-api/src/services/system/types.ts @@ -24,6 +24,7 @@ import { BackendFeature } from '../../types'; export type ServiceRef< TService, TScope extends 'root' | 'plugin' = 'root' | 'plugin', + TSingleton extends boolean = boolean, > = { id: string; @@ -38,6 +39,8 @@ export type ServiceRef< */ scope: TScope; + singleton: TSingleton; + /** * Utility for getting the type of the service, using `typeof serviceRef.T`. * Attempting to actually read this value will result in an exception. @@ -51,8 +54,9 @@ export type ServiceRef< export interface ServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TSingleton extends boolean = boolean, > extends BackendFeature { - service: ServiceRef; + service: ServiceRef; } /** @@ -62,21 +66,23 @@ export interface ServiceFactory< export interface ServiceFactoryCompat< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', + TSingleton extends boolean = boolean, TOpts extends object | undefined = undefined, -> extends ServiceFactory { +> extends ServiceFactory { /** * @deprecated Callable service factories will be removed in a future release, please re-implement the service factory using the available APIs instead. If no options are being passed, you can simply remove the trailing `()`. */ ( ...options: undefined extends TOpts ? [] : [options?: TOpts] - ): ServiceFactory; + ): ServiceFactory; } /** @internal */ export interface InternalServiceFactory< TService = unknown, TScope extends 'plugin' | 'root' = 'plugin' | 'root', -> extends ServiceFactory { + TSingleton extends boolean = boolean, +> extends ServiceFactory { version: 'v1'; initialization?: 'always' | 'lazy'; deps: { [key in string]: ServiceRef }; @@ -96,9 +102,14 @@ export interface InternalServiceFactory< export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory); /** @public */ -export interface ServiceRefOptions { +export interface ServiceRefOptions< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean, +> { id: string; scope?: TScope; + singleton?: TSingleton; defaultFactory?( service: ServiceRef, ): Promise; @@ -116,8 +127,8 @@ export interface ServiceRefOptions { * @public */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; /** * Creates a new service definition. This overload is used to create root scoped services. @@ -125,16 +136,34 @@ export function createServiceRef( * @public */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef; + options: ServiceRefOptions, +): ServiceRef; +/** + * Creates a new service definition. This overload is used to create plugin scoped services. + * + * @public + */ export function createServiceRef( - options: ServiceRefOptions, -): ServiceRef { - const { id, scope = 'plugin', defaultFactory } = options; + options: ServiceRefOptions, +): ServiceRef; + +/** + * Creates a new service definition. This overload is used to create root scoped services. + * + * @public + */ +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef; +export function createServiceRef( + options: ServiceRefOptions, +): ServiceRef { + const { id, scope = 'plugin', singleton = true, defaultFactory } = options; return { id, scope, + singleton, get T(): TService { throw new Error(`tried to read ServiceRef.T of ${this}`); }, @@ -143,7 +172,7 @@ export function createServiceRef( }, $$type: '@backstage/ServiceRef', __defaultFactory: defaultFactory, - } as ServiceRef & { + } as ServiceRef & { __defaultFactory?: ( service: ServiceRef, ) => Promise | (() => ServiceFactory)>; @@ -155,12 +184,17 @@ type ServiceRefsToInstances< T extends { [key in string]: ServiceRef }, TScope extends 'root' | 'plugin' = 'root' | 'plugin', > = { - [key in keyof T as T[key]['scope'] extends TScope ? key : never]: T[key]['T']; + [key in keyof T as T[key]['scope'] extends TScope + ? key + : never]: T[key]['singleton'] extends true + ? T[key]['T'] + : Array; }; /** @public */ export interface RootServiceFactoryOptions< - TService, + TService, // TODO(Rugvip): Can we forward the entire service ref type here instead of forwarding each type arg once the callback form is gone? + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, > { @@ -175,7 +209,7 @@ export interface RootServiceFactoryOptions< * Service factories for root scoped services use `always` as the default, while plugin scoped services use `lazy`. */ initialization?: 'always' | 'lazy'; - service: ServiceRef; + service: ServiceRef; deps: TDeps; factory(deps: ServiceRefsToInstances): TImpl | Promise; } @@ -183,6 +217,7 @@ export interface RootServiceFactoryOptions< /** @public */ export interface PluginServiceFactoryOptions< TService, + TSingleton extends boolean, TContext, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, @@ -198,7 +233,7 @@ export interface PluginServiceFactoryOptions< * Service factories for root scoped services use `always` as the default, while plugin scoped services use `lazy`. */ initialization?: 'always' | 'lazy'; - service: ServiceRef; + service: ServiceRef; deps: TDeps; createRootContext?( deps: ServiceRefsToInstances, @@ -217,12 +252,13 @@ export interface PluginServiceFactoryOptions< */ export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( - options: RootServiceFactoryOptions, -): ServiceFactoryCompat; + options: RootServiceFactoryOptions, +): ServiceFactoryCompat; /** * Creates a root scoped service factory with optional options. * @@ -235,14 +271,15 @@ export function createServiceFactory< */ export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TOpts extends object | undefined = undefined, >( options: ( options?: TOpts, - ) => RootServiceFactoryOptions, -): ServiceFactoryCompat; + ) => RootServiceFactoryOptions, +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory without options. * @@ -251,13 +288,20 @@ export function createServiceFactory< */ export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext = undefined, TOpts extends object | undefined = undefined, >( - options: PluginServiceFactoryOptions, -): ServiceFactoryCompat; + options: PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; /** * Creates a plugin scoped service factory with optional options. * @@ -270,6 +314,7 @@ export function createServiceFactory< */ export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext = undefined, @@ -277,25 +322,46 @@ export function createServiceFactory< >( options: ( options?: TOpts, - ) => PluginServiceFactoryOptions, -): ServiceFactoryCompat; + ) => PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >, +): ServiceFactoryCompat; export function createServiceFactory< TService, + TSingleton extends boolean, TImpl extends TService, TDeps extends { [name in string]: ServiceRef }, TContext, TOpts extends object | undefined = undefined, >( options: - | RootServiceFactoryOptions - | PluginServiceFactoryOptions - | ((options: TOpts) => RootServiceFactoryOptions) + | RootServiceFactoryOptions + | PluginServiceFactoryOptions | (( options: TOpts, - ) => PluginServiceFactoryOptions) - | (() => RootServiceFactoryOptions) - | (() => PluginServiceFactoryOptions), -): ServiceFactoryCompat { + ) => RootServiceFactoryOptions) + | (( + options: TOpts, + ) => PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >) + | (() => RootServiceFactoryOptions) + | (() => PluginServiceFactoryOptions< + TService, + TSingleton, + TContext, + TImpl, + TDeps + >), +): ServiceFactoryCompat { const configCallback = typeof options === 'function' ? options : () => options; const factory = ( @@ -303,18 +369,25 @@ export function createServiceFactory< ): InternalServiceFactory => { const anyConf = configCallback(o!); if (anyConf.service.scope === 'root') { - const c = anyConf as RootServiceFactoryOptions; + const c = anyConf as RootServiceFactoryOptions< + TService, + TSingleton, + TImpl, + TDeps + >; return { $$type: '@backstage/BackendFeature', version: 'v1', service: c.service, initialization: c.initialization, deps: c.deps, - factory: async (deps: TDeps) => c.factory(deps), + factory: async (deps: ServiceRefsToInstances) => + c.factory(deps), }; } const c = anyConf as PluginServiceFactoryOptions< TService, + TSingleton, TContext, TImpl, TDeps @@ -326,12 +399,14 @@ export function createServiceFactory< initialization: c.initialization, ...('createRootContext' in c ? { - createRootContext: async (deps: TDeps) => - c?.createRootContext?.(deps), + createRootContext: async ( + deps: ServiceRefsToInstances, + ) => c?.createRootContext?.(deps), } : {}), deps: c.deps, - factory: async (deps: TDeps, ctx: TContext) => c.factory(deps, ctx), + factory: async (deps: ServiceRefsToInstances, ctx: TContext) => + c.factory(deps, ctx), }; }; diff --git a/packages/backend-test-utils/api-report.md b/packages/backend-test-utils/api-report.md index 1f93573564..5a9e016dcd 100644 --- a/packages/backend-test-utils/api-report.md +++ b/packages/backend-test-utils/api-report.md @@ -156,7 +156,7 @@ export namespace mockServices { // (undocumented) export namespace auth { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -165,7 +165,7 @@ export namespace mockServices { // (undocumented) export namespace cache { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -174,7 +174,7 @@ export namespace mockServices { // (undocumented) export namespace database { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -185,7 +185,12 @@ export namespace mockServices { // (undocumented) export namespace discovery { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + DiscoveryService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -194,7 +199,7 @@ export namespace mockServices { // (undocumented) export namespace events { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -208,8 +213,8 @@ export namespace mockServices { export namespace httpAuth { const factory: ((options?: { defaultCredentials?: BackstageCredentials; - }) => ServiceFactory) & - ServiceFactory; + }) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -218,7 +223,12 @@ export namespace mockServices { // (undocumented) export namespace httpRouter { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + HttpRouterService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -229,7 +239,7 @@ export namespace mockServices { // (undocumented) export namespace identity { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -238,7 +248,12 @@ export namespace mockServices { // (undocumented) export namespace lifecycle { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + LifecycleService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -247,7 +262,7 @@ export namespace mockServices { // (undocumented) export namespace logger { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -256,7 +271,12 @@ export namespace mockServices { // (undocumented) export namespace permissions { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + PermissionsService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -271,15 +291,15 @@ export namespace mockServices { data?: JsonObject; }; const // (undocumented) - factory: ServiceFactory & + factory: ServiceFactory & (( options?: Options | undefined, - ) => ServiceFactory); + ) => ServiceFactory); } // (undocumented) export namespace rootHealth { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -290,8 +310,8 @@ export namespace mockServices { const // (undocumented) factory: (( options?: RootHttpRouterFactoryOptions | undefined, - ) => ServiceFactory) & - ServiceFactory; + ) => ServiceFactory) & + ServiceFactory; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -300,7 +320,12 @@ export namespace mockServices { // (undocumented) export namespace rootLifecycle { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + RootLifecycleService, + 'root', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -315,10 +340,10 @@ export namespace mockServices { level?: 'none' | 'error' | 'warn' | 'info' | 'debug'; }; const // (undocumented) - factory: ServiceFactory & + factory: ServiceFactory & (( options?: Options | undefined, - ) => ServiceFactory); + ) => ServiceFactory); const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -327,7 +352,12 @@ export namespace mockServices { // (undocumented) export namespace scheduler { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + SchedulerService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -338,7 +368,12 @@ export namespace mockServices { // (undocumented) export namespace tokenManager { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + TokenManagerService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -347,7 +382,12 @@ export namespace mockServices { // (undocumented) export namespace urlReader { const // (undocumented) - factory: ServiceFactoryCompat; + factory: ServiceFactoryCompat< + UrlReaderService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -358,7 +398,12 @@ export namespace mockServices { ): UserInfoService; // (undocumented) export namespace userInfo { - const factory: ServiceFactoryCompat; + const factory: ServiceFactoryCompat< + UserInfoService, + 'plugin', + true, + undefined + >; const // (undocumented) mock: ( partialImpl?: Partial | undefined, @@ -374,22 +419,34 @@ export function registerMswTestHooks(worker: { }): void; // @public -export class ServiceFactoryTester { - static from( - subject: ServiceFactory, +export class ServiceFactoryTester< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean = true, +> { + static from< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean = true, + >( + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, - ): ServiceFactoryTester; + ): ServiceFactoryTester; // @deprecated get( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise; - getService( - service: ServiceRef, + ): Promise; + getService< + TGetService, + TGetScope extends 'root' | 'plugin', + TGetSingleton extends boolean, + >( + service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] - ): Promise; + ): Promise; getSubject( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise; + ): Promise; } // @public diff --git a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts index aea0d9d270..94092aff68 100644 --- a/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts +++ b/packages/backend-test-utils/src/next/wiring/ServiceFactoryTester.ts @@ -43,8 +43,12 @@ export interface ServiceFactoryTesterOptions { * * @public */ -export class ServiceFactoryTester { - readonly #subject: ServiceRef; +export class ServiceFactoryTester< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean = true, +> { + readonly #subject: ServiceRef; readonly #registry: ServiceRegistry; /** @@ -54,10 +58,14 @@ export class ServiceFactoryTester { * @param options - Additional options * @returns A new tester instance for the provided subject. */ - static from( - subject: ServiceFactory, + static from< + TService, + TScope extends 'root' | 'plugin', + TSingleton extends boolean = true, + >( + subject: ServiceFactory, options?: ServiceFactoryTesterOptions, - ) { + ): ServiceFactoryTester { const registry = ServiceRegistry.create([ ...defaultServiceFactories, ...(options?.dependencies ?? []), @@ -67,7 +75,7 @@ export class ServiceFactoryTester { } private constructor( - subject: ServiceRef, + subject: ServiceRef, registry: ServiceRegistry, ) { this.#subject = subject; @@ -81,7 +89,7 @@ export class ServiceFactoryTester { */ async get( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { return this.getSubject(...args); } @@ -97,9 +105,10 @@ export class ServiceFactoryTester { */ async getSubject( ...args: 'root' extends TScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { const [pluginId] = args; - return this.#registry.get(this.#subject, pluginId ?? 'test')!; + const instance = this.#registry.get(this.#subject, pluginId ?? 'test')!; + return instance; } /** @@ -109,10 +118,14 @@ export class ServiceFactoryTester { * * A plugin ID can optionally be provided for plugin scoped services, otherwise the plugin ID 'test' is used. */ - async getService( - service: ServiceRef, + async getService< + TGetService, + TGetScope extends 'root' | 'plugin', + TGetSingleton extends boolean, + >( + service: ServiceRef, ...args: 'root' extends TGetScope ? [] : [pluginId?: string] - ): Promise { + ): Promise { const [pluginId] = args; const instance = await this.#registry.get(service, pluginId ?? 'test'); if (instance === undefined) { diff --git a/plugins/catalog-node/api-report-alpha.md b/plugins/catalog-node/api-report-alpha.md index b61d1b1d27..a2579e29f4 100644 --- a/plugins/catalog-node/api-report-alpha.md +++ b/plugins/catalog-node/api-report-alpha.md @@ -88,7 +88,7 @@ export interface CatalogProcessingExtensionPoint { export const catalogProcessingExtensionPoint: ExtensionPoint; // @alpha -export const catalogServiceRef: ServiceRef; +export const catalogServiceRef: ServiceRef; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/events-node/api-report.md b/plugins/events-node/api-report.md index 5b45b4419e..e159446103 100644 --- a/plugins/events-node/api-report.md +++ b/plugins/events-node/api-report.md @@ -66,11 +66,12 @@ export type EventsServiceEventHandler = (params: EventParams) => Promise; export const eventsServiceFactory: ServiceFactoryCompat< EventsService, 'plugin', + true, undefined >; // @public -export const eventsServiceRef: ServiceRef; +export const eventsServiceRef: ServiceRef; // @public (undocumented) export type EventsServiceSubscribeOptions = { diff --git a/plugins/notifications-node/api-report.md b/plugins/notifications-node/api-report.md index 37f28fec35..fd356131ee 100644 --- a/plugins/notifications-node/api-report.md +++ b/plugins/notifications-node/api-report.md @@ -65,7 +65,11 @@ export interface NotificationService { } // @public (undocumented) -export const notificationService: ServiceRef; +export const notificationService: ServiceRef< + NotificationService, + 'plugin', + true +>; // @public (undocumented) export type NotificationServiceOptions = { diff --git a/plugins/search-backend-node/api-report-alpha.md b/plugins/search-backend-node/api-report-alpha.md index 4f3015a0e4..dc41fa6400 100644 --- a/plugins/search-backend-node/api-report-alpha.md +++ b/plugins/search-backend-node/api-report-alpha.md @@ -46,7 +46,11 @@ export type SearchIndexServiceInitOptions = { }; // @alpha -export const searchIndexServiceRef: ServiceRef; +export const searchIndexServiceRef: ServiceRef< + SearchIndexService, + 'plugin', + true +>; // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/signals-node/api-report.md b/plugins/signals-node/api-report.md index cf4ab84aff..b2d5a2b210 100644 --- a/plugins/signals-node/api-report.md +++ b/plugins/signals-node/api-report.md @@ -37,7 +37,7 @@ export type SignalPayload = { export interface SignalService extends SignalsService {} // @public @deprecated (undocumented) -export const signalService: ServiceRef; +export const signalService: ServiceRef; // @public (undocumented) export interface SignalsService { @@ -52,7 +52,7 @@ export type SignalsServiceOptions = { }; // @public (undocumented) -export const signalsServiceRef: ServiceRef; +export const signalsServiceRef: ServiceRef; // (No @packageDocumentation comment for this package) ```