Merge pull request #25593 from backstage/mob/nbs10-service-modules

[NBS 1.0] Enabling multiple service implementations
This commit is contained in:
Patrik Oldsberg
2024-08-06 15:09:23 +02:00
committed by GitHub
40 changed files with 730 additions and 251 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-app-api': patch
---
Update the `ServiceRegister` implementation to enable registering multiple service implementations for a given service ref.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-test-utils': patch
---
Update the `ServiceFactoryTester` to be able to test services that enables multi implementation installation.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/backend-defaults': patch
---
Update the `UrlReader` service to depends on multiple instances of `UrlReaderFactoryProvider` service.
+58
View File
@@ -0,0 +1,58 @@
---
'@backstage/backend-plugin-api': minor
---
The `createServiceRef` function now accepts a new boolean `multiple` option. The `multiple` option defaults to `false` and when set to `true`, it enables that multiple implementation are installed for the created service ref.
We're looking for ways to make it possible to augment services without the need to replace the entire service.
Typical example of that being the ability to install support for additional targets for the `UrlReader` service without replacing the service itself. This achieves that by allowing us to define services that can have multiple simultaneous implementation, allowing the `UrlReader` implementation to depend on such a service to collect all possible implementation of support for external targets:
```diff
// @backstage/backend-defaults
+ export const urlReaderFactoriesServiceRef = createServiceRef<ReaderFactory>({
+ id: 'core.urlReader.factories',
+ scope: 'plugin',
+ multiton: true,
+ });
...
export const urlReaderServiceFactory = createServiceFactory({
service: coreServices.urlReader,
deps: {
config: coreServices.rootConfig,
logger: coreServices.logger,
+ factories: urlReaderFactoriesServiceRef,
},
- async factory({ config, logger }) {
+ async factory({ config, logger, factories }) {
return UrlReaders.default({
config,
logger,
+ factories,
});
},
});
```
With that, you can then add more custom `UrlReader` factories by installing more implementations of the `urlReaderFactoriesServiceRef` in your backend instance. Something like:
```ts
// packages/backend/index.ts
import { createServiceFactory } from '@backstage/backend-plugin-api';
import { urlReaderFactoriesServiceRef } from '@backstage/backend-defaults';
...
backend.add(createServiceFactory({
service: urlReaderFactoriesServiceRef,
deps: {},
async factory() {
return CustomUrlReader.factory;
},
}));
...
```
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/plugin-search-backend-node': patch
'@backstage/plugin-catalog-node': patch
---
Explicit declare if the service ref accepts `single` or `multiple` implementations.
@@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api';
export const featureDiscoveryServiceFactory: ServiceFactoryCompat<
FeatureDiscoveryService,
'root',
'singleton',
undefined
>;
+19 -2
View File
@@ -49,6 +49,7 @@ import { UserInfoService } from '@backstage/backend-plugin-api';
export const authServiceFactory: ServiceFactoryCompat<
AuthService,
'plugin',
'singleton',
undefined
>;
@@ -80,6 +81,7 @@ export interface Backend {
export const cacheServiceFactory: ServiceFactoryCompat<
CacheService,
'plugin',
'singleton',
undefined
>;
@@ -113,6 +115,7 @@ export interface CreateSpecializedBackendOptions {
export const databaseServiceFactory: ServiceFactoryCompat<
DatabaseService,
'plugin',
'singleton',
undefined
>;
@@ -135,6 +138,7 @@ export type DefaultRootHttpRouterOptions = DefaultRootHttpRouterOptions_2;
export const discoveryServiceFactory: ServiceFactoryCompat<
DiscoveryService,
'plugin',
'singleton',
undefined
>;
@@ -161,6 +165,7 @@ export class HostDiscovery implements DiscoveryService {
export const httpAuthServiceFactory: ServiceFactoryCompat<
HttpAuthService,
'plugin',
'singleton',
undefined
>;
@@ -168,6 +173,7 @@ export const httpAuthServiceFactory: ServiceFactoryCompat<
export const httpRouterServiceFactory: ServiceFactoryCompat<
HttpRouterService,
'plugin',
'singleton',
undefined
>;
@@ -191,6 +197,7 @@ export type IdentityFactoryOptions = {
export const identityServiceFactory: ServiceFactoryCompat<
IdentityService,
'plugin',
'singleton',
IdentityFactoryOptions
>;
@@ -203,6 +210,7 @@ export type LifecycleMiddlewareOptions = LifecycleMiddlewareOptions_2;
export const lifecycleServiceFactory: ServiceFactoryCompat<
LifecycleService,
'plugin',
'singleton',
undefined
>;
@@ -220,6 +228,7 @@ export function loadBackendConfig(options: {
export const loggerServiceFactory: ServiceFactoryCompat<
LoggerService,
'plugin',
'singleton',
undefined
>;
@@ -248,6 +257,7 @@ export type MiddlewareFactoryOptions = MiddlewareFactoryOptions_2;
export const permissionsServiceFactory: ServiceFactoryCompat<
PermissionsService,
'plugin',
'singleton',
undefined
>;
@@ -278,6 +288,7 @@ export interface RootConfigFactoryOptions {
export const rootConfigServiceFactory: ServiceFactoryCompat<
RootConfigService,
'root',
'singleton',
RootConfigFactoryOptions
>;
@@ -294,13 +305,14 @@ export type RootHttpRouterFactoryOptions = RootHttpRouterFactoryOptions_2;
// @public @deprecated (undocumented)
export const rootHttpRouterServiceFactory: ((
options?: RootHttpRouterFactoryOptions_2 | undefined,
) => ServiceFactory<RootHttpRouterService, 'root'>) &
ServiceFactory<RootHttpRouterService, 'root'>;
) => ServiceFactory<RootHttpRouterService, 'root', 'singleton'>) &
ServiceFactory<RootHttpRouterService, 'root', 'singleton'>;
// @public @deprecated
export const rootLifecycleServiceFactory: ServiceFactoryCompat<
RootLifecycleService,
'root',
'singleton',
undefined
>;
@@ -308,6 +320,7 @@ export const rootLifecycleServiceFactory: ServiceFactoryCompat<
export const rootLoggerServiceFactory: ServiceFactoryCompat<
RootLoggerService,
'root',
'singleton',
undefined
>;
@@ -315,6 +328,7 @@ export const rootLoggerServiceFactory: ServiceFactoryCompat<
export const schedulerServiceFactory: ServiceFactoryCompat<
SchedulerService,
'plugin',
'singleton',
undefined
>;
@@ -322,6 +336,7 @@ export const schedulerServiceFactory: ServiceFactoryCompat<
export const tokenManagerServiceFactory: ServiceFactoryCompat<
TokenManagerService,
'plugin',
'singleton',
undefined
>;
@@ -329,6 +344,7 @@ export const tokenManagerServiceFactory: ServiceFactoryCompat<
export const urlReaderServiceFactory: ServiceFactoryCompat<
UrlReaderService,
'plugin',
'singleton',
undefined
>;
@@ -336,6 +352,7 @@ export const urlReaderServiceFactory: ServiceFactoryCompat<
export const userInfoServiceFactory: ServiceFactoryCompat<
UserInfoService,
'plugin',
'singleton',
undefined
>;
@@ -58,12 +58,24 @@ function createPluginMetadataServiceFactory(pluginId: string) {
export class ServiceRegistry {
static create(factories: Array<ServiceFactory>): ServiceRegistry {
const registry = new ServiceRegistry(factories);
const factoryMap = new Map<string, InternalServiceFactory[]>();
for (const factory of factories) {
if (factory.service.multiton) {
const existing = factoryMap.get(factory.service.id) ?? [];
factoryMap.set(
factory.service.id,
existing.concat(toInternalServiceFactory(factory)),
);
} else {
factoryMap.set(factory.service.id, [toInternalServiceFactory(factory)]);
}
}
const registry = new ServiceRegistry(factoryMap);
registry.checkForCircularDeps();
return registry;
}
readonly #providedFactories: Map<string, InternalServiceFactory>;
readonly #providedFactories: Map<string, InternalServiceFactory[]>;
readonly #loadedDefaultFactories: Map<
Function,
Promise<InternalServiceFactory>
@@ -82,10 +94,8 @@ export class ServiceRegistry {
readonly #addedFactoryIds = new Set<string>();
readonly #instantiatedFactories = new Set<string>();
private constructor(factories: Array<ServiceFactory>) {
this.#providedFactories = new Map(
factories.map(sf => [sf.service.id, toInternalServiceFactory(sf)]),
);
private constructor(factories: Map<string, InternalServiceFactory[]>) {
this.#providedFactories = factories;
this.#loadedDefaultFactories = new Map();
this.#implementations = new Map();
}
@@ -93,17 +103,17 @@ export class ServiceRegistry {
#resolveFactory(
ref: ServiceRef<unknown>,
pluginId: string,
): Promise<InternalServiceFactory> | undefined {
): Promise<InternalServiceFactory[]> | 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>
| InternalServiceFactory
| Promise<InternalServiceFactory[]>
| 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.multiton) {
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.multiton) {
const newFactories = (
this.#providedFactories.get(factoryId) ?? []
).concat(toInternalServiceFactory(factory));
this.#providedFactories.set(factoryId, newFactories);
} else {
if (this.#addedFactoryIds.has(factoryId)) {
throw new Error(
`Duplicate service implementations provided for ${factoryId}`,
);
}
this.#addedFactoryIds.add(factoryId);
this.#providedFactories.set(factoryId, [
toInternalServiceFactory(factory),
]);
}
}
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<T>(ref: ServiceRef<T>, pluginId: string): Promise<T> | undefined {
get<T, TInstances extends 'singleton' | 'multiton'>(
ref: ServiceRef<T, 'plugin' | 'root', TInstances>,
pluginId: string,
): Promise<TInstances extends 'multiton' ? T[] : T> | 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<Promise<[name: string, impl: unknown]>>();
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.multiton
? (Promise.resolve([]) as
| Promise<TInstances extends 'multiton' ? T[] : T>
| undefined)
: 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<T>;
}
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<T>;
}
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<Promise<[name: string, impl: unknown]>>();
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<any>;
if (!result) {
const allDeps = new Array<
Promise<[name: string, impl: unknown]>
>();
let result = implementation.byPlugin.get(pluginId) as Promise<any>;
if (!result) {
const allDeps = new Array<Promise<[name: string, impl: unknown]>>();
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.multiton ? results : results[0]));
}
}
@@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api';
export const authServiceFactory: ServiceFactoryCompat<
AuthService,
'plugin',
'singleton',
undefined
>;
@@ -28,6 +28,7 @@ export type CacheManagerOptions = {
export const cacheServiceFactory: ServiceFactoryCompat<
CacheService,
'plugin',
'singleton',
undefined
>;
@@ -35,6 +35,7 @@ export type DatabaseManagerOptions = {
export const databaseServiceFactory: ServiceFactoryCompat<
DatabaseService,
'plugin',
'singleton',
undefined
>;
@@ -11,6 +11,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api';
export const discoveryServiceFactory: ServiceFactoryCompat<
DiscoveryService,
'plugin',
'singleton',
undefined
>;
@@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api';
export const httpAuthServiceFactory: ServiceFactoryCompat<
HttpAuthService,
'plugin',
'singleton',
undefined
>;
@@ -18,6 +18,7 @@ export function createLifecycleMiddleware(
export const httpRouterServiceFactory: ServiceFactoryCompat<
HttpRouterService,
'plugin',
'singleton',
undefined
>;
@@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api';
export const lifecycleServiceFactory: ServiceFactoryCompat<
LifecycleService,
'plugin',
'singleton',
undefined
>;
@@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api';
export const loggerServiceFactory: ServiceFactoryCompat<
LoggerService,
'plugin',
'singleton',
undefined
>;
@@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api';
export const permissionsServiceFactory: ServiceFactoryCompat<
PermissionsService,
'plugin',
'singleton',
undefined
>;
@@ -28,8 +28,8 @@ export interface RootConfigFactoryOptions {
// @public (undocumented)
export const rootConfigServiceFactory: ((
options?: RootConfigFactoryOptions,
) => ServiceFactory<RootConfigService, 'root'>) &
ServiceFactory<RootConfigService, 'root'>;
) => ServiceFactory<RootConfigService, 'root', 'singleton'>) &
ServiceFactory<RootConfigService, 'root', 'singleton'>;
// (No @packageDocumentation comment for this package)
```
@@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api';
export const rootHealthServiceFactory: ServiceFactoryCompat<
RootHealthService,
'root',
'singleton',
undefined
>;
@@ -151,8 +151,8 @@ export type RootHttpRouterFactoryOptions = {
// @public (undocumented)
export const rootHttpRouterServiceFactory: ((
options?: RootHttpRouterFactoryOptions,
) => ServiceFactory<RootHttpRouterService, 'root'>) &
ServiceFactory<RootHttpRouterService, 'root'>;
) => ServiceFactory<RootHttpRouterService, 'root', 'singleton'>) &
ServiceFactory<RootHttpRouterService, 'root', 'singleton'>;
// (No @packageDocumentation comment for this package)
```
@@ -10,6 +10,7 @@ import { ServiceFactoryCompat } from '@backstage/backend-plugin-api';
export const rootLifecycleServiceFactory: ServiceFactoryCompat<
RootLifecycleService,
'root',
'singleton',
undefined
>;
@@ -14,6 +14,7 @@ import { transport } from 'winston';
export const rootLoggerServiceFactory: ServiceFactoryCompat<
RootLoggerService,
'root',
'singleton',
undefined
>;
@@ -21,6 +21,7 @@ export class DefaultSchedulerService {
export const schedulerServiceFactory: ServiceFactoryCompat<
SchedulerService,
'plugin',
'singleton',
undefined
>;
@@ -22,6 +22,7 @@ import { HarnessIntegration } from '@backstage/integration';
import { LoggerService } from '@backstage/backend-plugin-api';
import { Readable } from 'stream';
import { ServiceFactoryCompat } from '@backstage/backend-plugin-api';
import { ServiceRef } from '@backstage/backend-plugin-api';
import { UrlReaderService } from '@backstage/backend-plugin-api';
import { UrlReaderServiceReadTreeOptions } from '@backstage/backend-plugin-api';
import { UrlReaderServiceReadTreeResponse } from '@backstage/backend-plugin-api';
@@ -413,6 +414,13 @@ export type ReadUrlResponseFactoryFromStreamOptions = {
lastModifiedAt?: Date;
};
// @public
export const urlReaderFactoriesServiceRef: ServiceRef<
ReaderFactory,
'plugin',
'multiton'
>;
// @public
export type UrlReaderPredicateTuple = {
predicate: (url: URL) => boolean;
@@ -429,6 +437,7 @@ export class UrlReaders {
export const urlReaderServiceFactory: ServiceFactoryCompat<
UrlReaderService,
'plugin',
'singleton',
undefined
>;
@@ -10,6 +10,7 @@ import { UserInfoService } from '@backstage/backend-plugin-api';
export const userInfoServiceFactory: ServiceFactoryCompat<
UserInfoService,
'plugin',
'singleton',
undefined
>;
@@ -15,4 +15,7 @@
*/
export * from './lib';
export { urlReaderServiceFactory } from './urlReaderServiceFactory';
export {
urlReaderServiceFactory,
urlReaderFactoriesServiceRef,
} from './urlReaderServiceFactory';
@@ -14,12 +14,36 @@
* 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: urlReaderFactoriesServiceRef,
* deps: {},
* async factory() {
* return CustomUrlReader.factory;
* },
* });
* ```
*/
export const urlReaderFactoriesServiceRef = createServiceRef<ReaderFactory>({
id: 'core.urlReader.factories',
scope: 'plugin',
multiton: true,
});
/**
* Reading content from external systems.
*
@@ -34,11 +58,13 @@ export const urlReaderServiceFactory = createServiceFactory({
deps: {
config: coreServices.rootConfig,
logger: coreServices.logger,
factories: urlReaderFactoriesServiceRef,
},
async factory({ config, logger }) {
async factory({ config, logger, factories }) {
return UrlReaders.default({
config,
logger,
factories,
});
},
});
@@ -117,6 +117,7 @@ export interface DynamicPluginsFactoryOptions {
export const dynamicPluginsFeatureDiscoveryServiceFactory: ServiceFactoryCompat<
FeatureDiscoveryService,
'root',
'singleton',
undefined
>;
@@ -127,6 +128,7 @@ export const dynamicPluginsFrontendSchemas: BackendFeatureCompat;
export const dynamicPluginsRootLoggerServiceFactory: ServiceFactoryCompat<
RootLoggerService,
'root',
'singleton',
undefined
>;
@@ -147,6 +149,7 @@ export interface DynamicPluginsSchemasService {
export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat<
DynamicPluginsSchemasService,
'root',
'singleton',
DynamicPluginsSchemasOptions
>;
@@ -154,13 +157,15 @@ export const dynamicPluginsSchemasServiceFactory: ServiceFactoryCompat<
export const dynamicPluginsServiceFactory: ServiceFactoryCompat<
DynamicPluginProvider,
'root',
'singleton',
DynamicPluginsFactoryOptions
>;
// @public (undocumented)
export const dynamicPluginsServiceRef: ServiceRef<
DynamicPluginProvider,
'root'
'root',
'singleton'
>;
// @public (undocumented)
@@ -17,7 +17,8 @@ export interface FeatureDiscoveryService {
// @alpha
export const featureDiscoveryServiceRef: ServiceRef<
FeatureDiscoveryService,
'root'
'root',
'singleton'
>;
// (No @packageDocumentation comment for this package)
+87 -42
View File
@@ -189,28 +189,32 @@ export type CacheServiceSetOptions = {
// @public
export namespace coreServices {
const auth: ServiceRef<AuthService, 'plugin'>;
const userInfo: ServiceRef<UserInfoService, 'plugin'>;
const cache: ServiceRef<CacheService, 'plugin'>;
const rootConfig: ServiceRef<RootConfigService, 'root'>;
const database: ServiceRef<DatabaseService, 'plugin'>;
const discovery: ServiceRef<DiscoveryService, 'plugin'>;
const rootHealth: ServiceRef<RootHealthService, 'root'>;
const httpAuth: ServiceRef<HttpAuthService, 'plugin'>;
const httpRouter: ServiceRef<HttpRouterService, 'plugin'>;
const lifecycle: ServiceRef<LifecycleService, 'plugin'>;
const logger: ServiceRef<LoggerService, 'plugin'>;
const permissions: ServiceRef<PermissionsService, 'plugin'>;
const pluginMetadata: ServiceRef<PluginMetadataService, 'plugin'>;
const rootHttpRouter: ServiceRef<RootHttpRouterService, 'root'>;
const rootLifecycle: ServiceRef<RootLifecycleService, 'root'>;
const rootLogger: ServiceRef<RootLoggerService, 'root'>;
const scheduler: ServiceRef<SchedulerService, 'plugin'>;
const auth: ServiceRef<AuthService, 'plugin', 'singleton'>;
const userInfo: ServiceRef<UserInfoService, 'plugin', 'singleton'>;
const cache: ServiceRef<CacheService, 'plugin', 'singleton'>;
const rootConfig: ServiceRef<RootConfigService, 'root', 'singleton'>;
const database: ServiceRef<DatabaseService, 'plugin', 'singleton'>;
const discovery: ServiceRef<DiscoveryService, 'plugin', 'singleton'>;
const rootHealth: ServiceRef<RootHealthService, 'root', 'singleton'>;
const httpAuth: ServiceRef<HttpAuthService, 'plugin', 'singleton'>;
const httpRouter: ServiceRef<HttpRouterService, 'plugin', 'singleton'>;
const lifecycle: ServiceRef<LifecycleService, 'plugin', 'singleton'>;
const logger: ServiceRef<LoggerService, 'plugin', 'singleton'>;
const permissions: ServiceRef<PermissionsService, 'plugin', 'singleton'>;
const pluginMetadata: ServiceRef<
PluginMetadataService,
'plugin',
'singleton'
>;
const rootHttpRouter: ServiceRef<RootHttpRouterService, 'root', 'singleton'>;
const rootLifecycle: ServiceRef<RootLifecycleService, 'root', 'singleton'>;
const rootLogger: ServiceRef<RootLoggerService, 'root', 'singleton'>;
const scheduler: ServiceRef<SchedulerService, 'plugin', 'singleton'>;
const // @deprecated
tokenManager: ServiceRef<TokenManagerService, 'plugin'>;
const urlReader: ServiceRef<UrlReaderService, 'plugin'>;
tokenManager: ServiceRef<TokenManagerService, 'plugin', 'singleton'>;
const urlReader: ServiceRef<UrlReaderService, 'plugin', 'singleton'>;
const // @deprecated
identity: ServiceRef<IdentityService, 'plugin'>;
identity: ServiceRef<IdentityService, 'plugin', 'singleton'>;
}
// @public
@@ -292,18 +296,20 @@ export interface CreateExtensionPointOptions {
// @public
export function createServiceFactory<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown, 'root'>;
},
TOpts extends object | undefined = undefined,
>(
options: RootServiceFactoryOptions<TService, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'root'>;
options: RootServiceFactoryOptions<TService, TInstances, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'root', TInstances>;
// @public @deprecated
export function createServiceFactory<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown, 'root'>;
@@ -312,12 +318,13 @@ export function createServiceFactory<
>(
options: (
options?: TOpts,
) => RootServiceFactoryOptions<TService, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'root', TOpts>;
) => RootServiceFactoryOptions<TService, TInstances, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'root', TInstances, TOpts>;
// @public
export function createServiceFactory<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
@@ -325,12 +332,19 @@ export function createServiceFactory<
TContext = undefined,
TOpts extends object | undefined = undefined,
>(
options: PluginServiceFactoryOptions<TService, TContext, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'plugin'>;
options: PluginServiceFactoryOptions<
TService,
TInstances,
TContext,
TImpl,
TDeps
>,
): ServiceFactoryCompat<TService, 'plugin', TInstances>;
// @public @deprecated
export function createServiceFactory<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
@@ -340,18 +354,34 @@ export function createServiceFactory<
>(
options: (
options?: TOpts,
) => PluginServiceFactoryOptions<TService, TContext, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'plugin', TOpts>;
) => PluginServiceFactoryOptions<
TService,
TInstances,
TContext,
TImpl,
TDeps
>,
): ServiceFactoryCompat<TService, 'plugin', TInstances, TOpts>;
// @public
export function createServiceRef<TService>(
options: ServiceRefOptions<TService, 'plugin'>,
): ServiceRef<TService, 'plugin'>;
options: ServiceRefOptions<TService, 'plugin', 'singleton'>,
): ServiceRef<TService, 'plugin', 'singleton'>;
// @public
export function createServiceRef<TService>(
options: ServiceRefOptions<TService, 'root'>,
): ServiceRef<TService, 'root'>;
options: ServiceRefOptions<TService, 'root', 'singleton'>,
): ServiceRef<TService, 'root', 'singleton'>;
// @public
export function createServiceRef<TService>(
options: ServiceRefOptions<TService, 'plugin', 'multiton'>,
): ServiceRef<TService, 'plugin', 'multiton'>;
// @public
export function createServiceRef<TService>(
options: ServiceRefOptions<TService, 'root', 'multiton'>,
): ServiceRef<TService, 'root', 'multiton'>;
// @public
export interface DatabaseService {
@@ -488,16 +518,18 @@ export interface PluginMetadataService {
// @public @deprecated (undocumented)
export type PluginServiceFactoryConfig<
TService,
TInstances extends 'singleton' | 'multiton',
TContext,
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
> = PluginServiceFactoryOptions<TService, TContext, TImpl, TDeps>;
> = PluginServiceFactoryOptions<TService, TInstances, TContext, TImpl, TDeps>;
// @public (undocumented)
export interface PluginServiceFactoryOptions<
TService,
TInstances extends 'singleton' | 'multiton',
TContext,
TImpl extends TService,
TDeps extends {
@@ -517,7 +549,7 @@ export interface PluginServiceFactoryOptions<
): TImpl | Promise<TImpl>;
initialization?: 'always' | 'lazy';
// (undocumented)
service: ServiceRef<TService, 'plugin'>;
service: ServiceRef<TService, 'plugin', TInstances>;
}
// @public
@@ -579,15 +611,17 @@ export interface RootLoggerService extends LoggerService {}
// @public @deprecated (undocumented)
export type RootServiceFactoryConfig<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
> = RootServiceFactoryOptions<TService, TImpl, TDeps>;
> = RootServiceFactoryOptions<TService, TInstances, TImpl, TDeps>;
// @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?
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
@@ -599,7 +633,7 @@ export interface RootServiceFactoryOptions<
factory(deps: ServiceRefsToInstances<TDeps, 'root'>): TImpl | Promise<TImpl>;
initialization?: 'always' | 'lazy';
// (undocumented)
service: ServiceRef<TService, 'root'>;
service: ServiceRef<TService, 'root', TInstances>;
}
// @public
@@ -690,21 +724,23 @@ export type SearchResponseFile = UrlReaderServiceSearchResponseFile;
export interface ServiceFactory<
TService = unknown,
TScope extends 'plugin' | 'root' = 'plugin' | 'root',
TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton',
> extends BackendFeature {
// (undocumented)
service: ServiceRef<TService, TScope>;
service: ServiceRef<TService, TScope, TInstances>;
}
// @public @deprecated (undocumented)
export interface ServiceFactoryCompat<
TService = unknown,
TScope extends 'plugin' | 'root' = 'plugin' | 'root',
TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton',
TOpts extends object | undefined = undefined,
> extends ServiceFactory<TService, TScope> {
> extends ServiceFactory<TService, TScope, TInstances> {
// @deprecated (undocumented)
(
...options: undefined extends TOpts ? [] : [options?: TOpts]
): ServiceFactory<TService, TScope>;
): ServiceFactory<TService, TScope, TInstances>;
}
// @public @deprecated
@@ -714,9 +750,11 @@ export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory);
export type ServiceRef<
TService,
TScope extends 'root' | 'plugin' = 'root' | 'plugin',
TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton',
> = {
id: string;
scope: TScope;
multiton?: TInstances extends 'multiton' ? true : false;
T: TService;
$$type: '@backstage/ServiceRef';
};
@@ -725,10 +763,15 @@ export type ServiceRef<
export type ServiceRefConfig<
TService,
TScope extends 'root' | 'plugin',
> = ServiceRefOptions<TService, TScope>;
TInstances extends 'singleton' | 'multiton',
> = ServiceRefOptions<TService, TScope, TInstances>;
// @public (undocumented)
export interface ServiceRefOptions<TService, TScope extends 'root' | 'plugin'> {
export interface ServiceRefOptions<
TService,
TScope extends 'root' | 'plugin',
TInstances extends 'singleton' | 'multiton',
> {
// (undocumented)
defaultFactory?(
service: ServiceRef<TService, TScope>,
@@ -740,6 +783,8 @@ export interface ServiceRefOptions<TService, TScope extends 'root' | 'plugin'> {
// (undocumented)
id: string;
// (undocumented)
multiton?: TInstances extends 'multiton' ? true : false;
// (undocumented)
scope?: TScope;
}
@@ -28,7 +28,8 @@ import {
export type ServiceRefConfig<
TService,
TScope extends 'root' | 'plugin',
> = ServiceRefOptions<TService, TScope>;
TInstances extends 'singleton' | 'multiton',
> = ServiceRefOptions<TService, TScope, TInstances>;
/**
* @public
@@ -36,9 +37,10 @@ export type ServiceRefConfig<
*/
export type RootServiceFactoryConfig<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
> = RootServiceFactoryOptions<TService, TImpl, TDeps>;
> = RootServiceFactoryOptions<TService, TInstances, TImpl, TDeps>;
/**
* @public
@@ -46,7 +48,8 @@ export type RootServiceFactoryConfig<
*/
export type PluginServiceFactoryConfig<
TService,
TInstances extends 'singleton' | 'multiton',
TContext,
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
> = PluginServiceFactoryOptions<TService, TContext, TImpl, TDeps>;
> = PluginServiceFactoryOptions<TService, TInstances, TContext, TImpl, TDeps>;
@@ -332,6 +332,24 @@ describe('createServiceFactory', () => {
metaFactory();
});
it('should support old service refs without a multiton field', () => {
const oldPluginDep = pluginDep as Omit<typeof pluginDep, 'multiton'>; // Old refs don't have a multiton field
const metaFactory = createServiceFactory({
service: ref,
deps: {
plugin: oldPluginDep,
},
async factory({ plugin }) {
const plugin1: boolean = plugin;
// @ts-expect-error
const plugin2: number = plugin;
unused(plugin1, plugin2);
return 'x';
},
});
expect(metaFactory).toEqual(expect.any(Function));
});
it('should only allow objects as options', () => {
// @ts-expect-error
const metaFactory = createServiceFactory((_opts: string) => ({
@@ -24,6 +24,7 @@ import { BackendFeature } from '../../types';
export type ServiceRef<
TService,
TScope extends 'root' | 'plugin' = 'root' | 'plugin',
TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton',
> = {
id: string;
@@ -38,6 +39,8 @@ export type ServiceRef<
*/
scope: TScope;
multiton?: TInstances extends 'multiton' ? true : false;
/**
* 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',
TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton',
> extends BackendFeature {
service: ServiceRef<TService, TScope>;
service: ServiceRef<TService, TScope, TInstances>;
}
/**
@@ -62,21 +66,23 @@ export interface ServiceFactory<
export interface ServiceFactoryCompat<
TService = unknown,
TScope extends 'plugin' | 'root' = 'plugin' | 'root',
TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton',
TOpts extends object | undefined = undefined,
> extends ServiceFactory<TService, TScope> {
> extends ServiceFactory<TService, TScope, TInstances> {
/**
* @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<TService, TScope>;
): ServiceFactory<TService, TScope, TInstances>;
}
/** @internal */
export interface InternalServiceFactory<
TService = unknown,
TScope extends 'plugin' | 'root' = 'plugin' | 'root',
> extends ServiceFactory<TService, TScope> {
TInstances extends 'singleton' | 'multiton' = 'singleton' | 'multiton',
> extends ServiceFactory<TService, TScope, TInstances> {
version: 'v1';
featureType: 'service';
initialization?: 'always' | 'lazy';
@@ -97,9 +103,14 @@ export interface InternalServiceFactory<
export type ServiceFactoryOrFunction = ServiceFactory | (() => ServiceFactory);
/** @public */
export interface ServiceRefOptions<TService, TScope extends 'root' | 'plugin'> {
export interface ServiceRefOptions<
TService,
TScope extends 'root' | 'plugin',
TInstances extends 'singleton' | 'multiton',
> {
id: string;
scope?: TScope;
multiton?: TInstances extends 'multiton' ? true : false;
defaultFactory?(
service: ServiceRef<TService, TScope>,
): Promise<ServiceFactory>;
@@ -117,8 +128,8 @@ export interface ServiceRefOptions<TService, TScope extends 'root' | 'plugin'> {
* @public
*/
export function createServiceRef<TService>(
options: ServiceRefOptions<TService, 'plugin'>,
): ServiceRef<TService, 'plugin'>;
options: ServiceRefOptions<TService, 'plugin', 'singleton'>,
): ServiceRef<TService, 'plugin', 'singleton'>;
/**
* Creates a new service definition. This overload is used to create root scoped services.
@@ -126,16 +137,37 @@ export function createServiceRef<TService>(
* @public
*/
export function createServiceRef<TService>(
options: ServiceRefOptions<TService, 'root'>,
): ServiceRef<TService, 'root'>;
options: ServiceRefOptions<TService, 'root', 'singleton'>,
): ServiceRef<TService, 'root', 'singleton'>;
/**
* Creates a new service definition. This overload is used to create plugin scoped services.
*
* @public
*/
export function createServiceRef<TService>(
options: ServiceRefOptions<TService, any>,
): ServiceRef<TService, any> {
const { id, scope = 'plugin', defaultFactory } = options;
options: ServiceRefOptions<TService, 'plugin', 'multiton'>,
): ServiceRef<TService, 'plugin', 'multiton'>;
/**
* Creates a new service definition. This overload is used to create root scoped services.
*
* @public
*/
export function createServiceRef<TService>(
options: ServiceRefOptions<TService, 'root', 'multiton'>,
): ServiceRef<TService, 'root', 'multiton'>;
export function createServiceRef<
TService,
TInstances extends 'singleton' | 'multiton',
>(
options: ServiceRefOptions<TService, any, TInstances>,
): ServiceRef<TService, any, TInstances> {
const { id, scope = 'plugin', multiton = false, defaultFactory } = options;
return {
id,
scope,
multiton,
get T(): TService {
throw new Error(`tried to read ServiceRef.T of ${this}`);
},
@@ -144,7 +176,7 @@ export function createServiceRef<TService>(
},
$$type: '@backstage/ServiceRef',
__defaultFactory: defaultFactory,
} as ServiceRef<TService, typeof scope> & {
} as ServiceRef<TService, typeof scope, TInstances> & {
__defaultFactory?: (
service: ServiceRef<TService>,
) => Promise<ServiceFactory<TService> | (() => ServiceFactory<TService>)>;
@@ -156,12 +188,17 @@ type ServiceRefsToInstances<
T extends { [key in string]: ServiceRef<unknown> },
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]['multiton'] extends true | undefined
? Array<T[key]['T']>
: T[key]['T'];
};
/** @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?
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
> {
@@ -176,7 +213,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<TService, 'root'>;
service: ServiceRef<TService, 'root', TInstances>;
deps: TDeps;
factory(deps: ServiceRefsToInstances<TDeps, 'root'>): TImpl | Promise<TImpl>;
}
@@ -184,6 +221,7 @@ export interface RootServiceFactoryOptions<
/** @public */
export interface PluginServiceFactoryOptions<
TService,
TInstances extends 'singleton' | 'multiton',
TContext,
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
@@ -199,7 +237,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<TService, 'plugin'>;
service: ServiceRef<TService, 'plugin', TInstances>;
deps: TDeps;
createRootContext?(
deps: ServiceRefsToInstances<TDeps, 'root'>,
@@ -218,12 +256,13 @@ export interface PluginServiceFactoryOptions<
*/
export function createServiceFactory<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown, 'root'> },
TOpts extends object | undefined = undefined,
>(
options: RootServiceFactoryOptions<TService, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'root'>;
options: RootServiceFactoryOptions<TService, TInstances, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'root', TInstances>;
/**
* Creates a root scoped service factory with optional options.
*
@@ -236,14 +275,15 @@ export function createServiceFactory<
*/
export function createServiceFactory<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown, 'root'> },
TOpts extends object | undefined = undefined,
>(
options: (
options?: TOpts,
) => RootServiceFactoryOptions<TService, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'root', TOpts>;
) => RootServiceFactoryOptions<TService, TInstances, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'root', TInstances, TOpts>;
/**
* Creates a plugin scoped service factory without options.
*
@@ -252,13 +292,20 @@ export function createServiceFactory<
*/
export function createServiceFactory<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TContext = undefined,
TOpts extends object | undefined = undefined,
>(
options: PluginServiceFactoryOptions<TService, TContext, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'plugin'>;
options: PluginServiceFactoryOptions<
TService,
TInstances,
TContext,
TImpl,
TDeps
>,
): ServiceFactoryCompat<TService, 'plugin', TInstances>;
/**
* Creates a plugin scoped service factory with optional options.
*
@@ -271,6 +318,7 @@ export function createServiceFactory<
*/
export function createServiceFactory<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TContext = undefined,
@@ -278,25 +326,51 @@ export function createServiceFactory<
>(
options: (
options?: TOpts,
) => PluginServiceFactoryOptions<TService, TContext, TImpl, TDeps>,
): ServiceFactoryCompat<TService, 'plugin', TOpts>;
) => PluginServiceFactoryOptions<
TService,
TInstances,
TContext,
TImpl,
TDeps
>,
): ServiceFactoryCompat<TService, 'plugin', TInstances, TOpts>;
export function createServiceFactory<
TService,
TInstances extends 'singleton' | 'multiton',
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TContext,
TOpts extends object | undefined = undefined,
>(
options:
| RootServiceFactoryOptions<TService, TImpl, TDeps>
| PluginServiceFactoryOptions<TService, TContext, TImpl, TDeps>
| ((options: TOpts) => RootServiceFactoryOptions<TService, TImpl, TDeps>)
| RootServiceFactoryOptions<TService, TInstances, TImpl, TDeps>
| PluginServiceFactoryOptions<TService, TInstances, TContext, TImpl, TDeps>
| ((
options: TOpts,
) => PluginServiceFactoryOptions<TService, TContext, TImpl, TDeps>)
| (() => RootServiceFactoryOptions<TService, TImpl, TDeps>)
| (() => PluginServiceFactoryOptions<TService, TContext, TImpl, TDeps>),
): ServiceFactoryCompat<TService, 'root' | 'plugin', TOpts> {
) => RootServiceFactoryOptions<TService, TInstances, TImpl, TDeps>)
| ((
options: TOpts,
) => PluginServiceFactoryOptions<
TService,
TInstances,
TContext,
TImpl,
TDeps
>)
| (() => RootServiceFactoryOptions<TService, TInstances, TImpl, TDeps>)
| (() => PluginServiceFactoryOptions<
TService,
TInstances,
TContext,
TImpl,
TDeps
>),
): ServiceFactoryCompat<
TService,
'root' | 'plugin',
'singleton' | 'multiton',
TOpts
> {
const configCallback =
typeof options === 'function' ? options : () => options;
const factory = (
@@ -304,7 +378,12 @@ export function createServiceFactory<
): InternalServiceFactory<TService, 'plugin' | 'root'> => {
const anyConf = configCallback(o!);
if (anyConf.service.scope === 'root') {
const c = anyConf as RootServiceFactoryOptions<TService, TImpl, TDeps>;
const c = anyConf as RootServiceFactoryOptions<
TService,
TInstances,
TImpl,
TDeps
>;
return {
$$type: '@backstage/BackendFeature',
version: 'v1',
@@ -312,11 +391,13 @@ export function createServiceFactory<
service: c.service,
initialization: c.initialization,
deps: c.deps,
factory: async (deps: TDeps) => c.factory(deps),
factory: async (deps: ServiceRefsToInstances<TDeps, 'root'>) =>
c.factory(deps),
};
}
const c = anyConf as PluginServiceFactoryOptions<
TService,
TInstances,
TContext,
TImpl,
TDeps
@@ -329,12 +410,14 @@ export function createServiceFactory<
initialization: c.initialization,
...('createRootContext' in c
? {
createRootContext: async (deps: TDeps) =>
c?.createRootContext?.(deps),
createRootContext: async (
deps: ServiceRefsToInstances<TDeps, 'root'>,
) => c?.createRootContext?.(deps),
}
: {}),
deps: c.deps,
factory: async (deps: TDeps, ctx: TContext) => c.factory(deps, ctx),
factory: async (deps: ServiceRefsToInstances<TDeps>, ctx: TContext) =>
c.factory(deps, ctx),
};
};
+133 -33
View File
@@ -156,7 +156,12 @@ export namespace mockServices {
// (undocumented)
export namespace auth {
const // (undocumented)
factory: ServiceFactoryCompat<AuthService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
AuthService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<AuthService> | undefined,
@@ -165,7 +170,12 @@ export namespace mockServices {
// (undocumented)
export namespace cache {
const // (undocumented)
factory: ServiceFactoryCompat<CacheService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
CacheService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<CacheService> | undefined,
@@ -174,7 +184,12 @@ export namespace mockServices {
// (undocumented)
export namespace database {
const // (undocumented)
factory: ServiceFactoryCompat<DatabaseService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
DatabaseService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<DatabaseService> | undefined,
@@ -185,7 +200,12 @@ export namespace mockServices {
// (undocumented)
export namespace discovery {
const // (undocumented)
factory: ServiceFactoryCompat<DiscoveryService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
DiscoveryService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<DiscoveryService> | undefined,
@@ -194,7 +214,12 @@ export namespace mockServices {
// (undocumented)
export namespace events {
const // (undocumented)
factory: ServiceFactoryCompat<EventsService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
EventsService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<EventsService> | undefined,
@@ -208,8 +233,8 @@ export namespace mockServices {
export namespace httpAuth {
const factory: ((options?: {
defaultCredentials?: BackstageCredentials;
}) => ServiceFactory<HttpAuthService, 'plugin'>) &
ServiceFactory<HttpAuthService, 'plugin'>;
}) => ServiceFactory<HttpAuthService, 'plugin', 'singleton'>) &
ServiceFactory<HttpAuthService, 'plugin', 'singleton'>;
const // (undocumented)
mock: (
partialImpl?: Partial<HttpAuthService> | undefined,
@@ -218,7 +243,12 @@ export namespace mockServices {
// (undocumented)
export namespace httpRouter {
const // (undocumented)
factory: ServiceFactoryCompat<HttpRouterService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
HttpRouterService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<HttpRouterService> | undefined,
@@ -229,7 +259,12 @@ export namespace mockServices {
// (undocumented)
export namespace identity {
const // (undocumented)
factory: ServiceFactoryCompat<IdentityService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
IdentityService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<IdentityService> | undefined,
@@ -238,7 +273,12 @@ export namespace mockServices {
// (undocumented)
export namespace lifecycle {
const // (undocumented)
factory: ServiceFactoryCompat<LifecycleService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
LifecycleService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<LifecycleService> | undefined,
@@ -247,7 +287,12 @@ export namespace mockServices {
// (undocumented)
export namespace logger {
const // (undocumented)
factory: ServiceFactoryCompat<LoggerService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
LoggerService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<LoggerService> | undefined,
@@ -256,7 +301,12 @@ export namespace mockServices {
// (undocumented)
export namespace permissions {
const // (undocumented)
factory: ServiceFactoryCompat<PermissionsService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
PermissionsService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<PermissionsService> | undefined,
@@ -271,15 +321,28 @@ export namespace mockServices {
data?: JsonObject;
};
const // (undocumented)
factory: ServiceFactory<RootConfigService, 'root'> &
factory: ServiceFactory<
RootConfigService,
'root',
'singleton' | 'multiton'
> &
((
options?: Options | undefined,
) => ServiceFactory<RootConfigService, 'root'>);
) => ServiceFactory<
RootConfigService,
'root',
'singleton' | 'multiton'
>);
}
// (undocumented)
export namespace rootHealth {
const // (undocumented)
factory: ServiceFactoryCompat<RootHealthService, 'root', undefined>;
factory: ServiceFactoryCompat<
RootHealthService,
'root',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<RootHealthService> | undefined,
@@ -290,8 +353,8 @@ export namespace mockServices {
const // (undocumented)
factory: ((
options?: RootHttpRouterFactoryOptions | undefined,
) => ServiceFactory<RootHttpRouterService, 'root'>) &
ServiceFactory<RootHttpRouterService, 'root'>;
) => ServiceFactory<RootHttpRouterService, 'root', 'singleton'>) &
ServiceFactory<RootHttpRouterService, 'root', 'singleton'>;
const // (undocumented)
mock: (
partialImpl?: Partial<RootHttpRouterService> | undefined,
@@ -300,7 +363,12 @@ export namespace mockServices {
// (undocumented)
export namespace rootLifecycle {
const // (undocumented)
factory: ServiceFactoryCompat<RootLifecycleService, 'root', undefined>;
factory: ServiceFactoryCompat<
RootLifecycleService,
'root',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<RootLifecycleService> | undefined,
@@ -315,10 +383,10 @@ export namespace mockServices {
level?: 'none' | 'error' | 'warn' | 'info' | 'debug';
};
const // (undocumented)
factory: ServiceFactory<LoggerService, 'root'> &
factory: ServiceFactory<LoggerService, 'root', 'singleton' | 'multiton'> &
((
options?: Options | undefined,
) => ServiceFactory<LoggerService, 'root'>);
) => ServiceFactory<LoggerService, 'root', 'singleton' | 'multiton'>);
const // (undocumented)
mock: (
partialImpl?: Partial<RootLoggerService> | undefined,
@@ -327,7 +395,12 @@ export namespace mockServices {
// (undocumented)
export namespace scheduler {
const // (undocumented)
factory: ServiceFactoryCompat<SchedulerService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
SchedulerService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<SchedulerService> | undefined,
@@ -338,7 +411,12 @@ export namespace mockServices {
// (undocumented)
export namespace tokenManager {
const // (undocumented)
factory: ServiceFactoryCompat<TokenManagerService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
TokenManagerService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<TokenManagerService> | undefined,
@@ -347,7 +425,12 @@ export namespace mockServices {
// (undocumented)
export namespace urlReader {
const // (undocumented)
factory: ServiceFactoryCompat<UrlReaderService, 'plugin', undefined>;
factory: ServiceFactoryCompat<
UrlReaderService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<UrlReaderService> | undefined,
@@ -358,7 +441,12 @@ export namespace mockServices {
): UserInfoService;
// (undocumented)
export namespace userInfo {
const factory: ServiceFactoryCompat<UserInfoService, 'plugin', undefined>;
const factory: ServiceFactoryCompat<
UserInfoService,
'plugin',
'singleton',
undefined
>;
const // (undocumented)
mock: (
partialImpl?: Partial<UserInfoService> | undefined,
@@ -374,22 +462,34 @@ export function registerMswTestHooks(worker: {
}): void;
// @public
export class ServiceFactoryTester<TService, TScope extends 'root' | 'plugin'> {
static from<TService, TScope extends 'root' | 'plugin'>(
subject: ServiceFactory<TService, TScope>,
export class ServiceFactoryTester<
TService,
TScope extends 'root' | 'plugin',
TInstances extends 'singleton' | 'multiton' = 'singleton',
> {
static from<
TService,
TScope extends 'root' | 'plugin',
TInstances extends 'singleton' | 'multiton' = 'singleton',
>(
subject: ServiceFactory<TService, TScope, TInstances>,
options?: ServiceFactoryTesterOptions,
): ServiceFactoryTester<TService, TScope>;
): ServiceFactoryTester<TService, TScope, TInstances>;
// @deprecated
get(
...args: 'root' extends TScope ? [] : [pluginId?: string]
): Promise<TService>;
getService<TGetService, TGetScope extends 'root' | 'plugin'>(
service: ServiceRef<TGetService, TGetScope>,
): Promise<TInstances extends 'multiton' ? TService[] : TService>;
getService<
TGetService,
TGetScope extends 'root' | 'plugin',
TGetInstances extends 'singleton' | 'multiton' = 'singleton',
>(
service: ServiceRef<TGetService, TGetScope, TGetInstances>,
...args: 'root' extends TGetScope ? [] : [pluginId?: string]
): Promise<TGetService>;
): Promise<TGetInstances extends 'multiton' ? TGetService[] : TGetService>;
getSubject(
...args: 'root' extends TScope ? [] : [pluginId?: string]
): Promise<TService>;
): Promise<TInstances extends 'multiton' ? TService[] : TService>;
}
// @public
@@ -43,8 +43,12 @@ export interface ServiceFactoryTesterOptions {
*
* @public
*/
export class ServiceFactoryTester<TService, TScope extends 'root' | 'plugin'> {
readonly #subject: ServiceRef<TService, TScope>;
export class ServiceFactoryTester<
TService,
TScope extends 'root' | 'plugin',
TInstances extends 'singleton' | 'multiton' = 'singleton',
> {
readonly #subject: ServiceRef<TService, TScope, TInstances>;
readonly #registry: ServiceRegistry;
/**
@@ -54,10 +58,14 @@ export class ServiceFactoryTester<TService, TScope extends 'root' | 'plugin'> {
* @param options - Additional options
* @returns A new tester instance for the provided subject.
*/
static from<TService, TScope extends 'root' | 'plugin'>(
subject: ServiceFactory<TService, TScope>,
static from<
TService,
TScope extends 'root' | 'plugin',
TInstances extends 'singleton' | 'multiton' = 'singleton',
>(
subject: ServiceFactory<TService, TScope, TInstances>,
options?: ServiceFactoryTesterOptions,
) {
): ServiceFactoryTester<TService, TScope, TInstances> {
const registry = ServiceRegistry.create([
...defaultServiceFactories,
...(options?.dependencies ?? []),
@@ -67,7 +75,7 @@ export class ServiceFactoryTester<TService, TScope extends 'root' | 'plugin'> {
}
private constructor(
subject: ServiceRef<TService, TScope>,
subject: ServiceRef<TService, TScope, TInstances>,
registry: ServiceRegistry,
) {
this.#subject = subject;
@@ -81,7 +89,7 @@ export class ServiceFactoryTester<TService, TScope extends 'root' | 'plugin'> {
*/
async get(
...args: 'root' extends TScope ? [] : [pluginId?: string]
): Promise<TService> {
): Promise<TInstances extends 'multiton' ? TService[] : TService> {
return this.getSubject(...args);
}
@@ -97,9 +105,10 @@ export class ServiceFactoryTester<TService, TScope extends 'root' | 'plugin'> {
*/
async getSubject(
...args: 'root' extends TScope ? [] : [pluginId?: string]
): Promise<TService> {
): Promise<TInstances extends 'multiton' ? TService[] : TService> {
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<TService, TScope extends 'root' | 'plugin'> {
*
* A plugin ID can optionally be provided for plugin scoped services, otherwise the plugin ID 'test' is used.
*/
async getService<TGetService, TGetScope extends 'root' | 'plugin'>(
service: ServiceRef<TGetService, TGetScope>,
async getService<
TGetService,
TGetScope extends 'root' | 'plugin',
TGetInstances extends 'singleton' | 'multiton' = 'singleton',
>(
service: ServiceRef<TGetService, TGetScope, TGetInstances>,
...args: 'root' extends TGetScope ? [] : [pluginId?: string]
): Promise<TGetService> {
): Promise<TGetInstances extends 'multiton' ? TGetService[] : TGetService> {
const [pluginId] = args;
const instance = await this.#registry.get(service, pluginId ?? 'test');
if (instance === undefined) {
+1 -1
View File
@@ -96,7 +96,7 @@ export interface CatalogProcessingExtensionPoint {
export const catalogProcessingExtensionPoint: ExtensionPoint<CatalogProcessingExtensionPoint>;
// @alpha
export const catalogServiceRef: ServiceRef<CatalogApi, 'plugin'>;
export const catalogServiceRef: ServiceRef<CatalogApi, 'plugin', 'singleton'>;
// (No @packageDocumentation comment for this package)
```
+2 -1
View File
@@ -66,11 +66,12 @@ export type EventsServiceEventHandler = (params: EventParams) => Promise<void>;
export const eventsServiceFactory: ServiceFactoryCompat<
EventsService,
'plugin',
'singleton',
undefined
>;
// @public
export const eventsServiceRef: ServiceRef<EventsService, 'plugin'>;
export const eventsServiceRef: ServiceRef<EventsService, 'plugin', 'singleton'>;
// @public (undocumented)
export type EventsServiceSubscribeOptions = {
+5 -1
View File
@@ -65,7 +65,11 @@ export interface NotificationService {
}
// @public (undocumented)
export const notificationService: ServiceRef<NotificationService, 'plugin'>;
export const notificationService: ServiceRef<
NotificationService,
'plugin',
'singleton'
>;
// @public (undocumented)
export type NotificationServiceOptions = {
@@ -46,7 +46,11 @@ export type SearchIndexServiceInitOptions = {
};
// @alpha
export const searchIndexServiceRef: ServiceRef<SearchIndexService, 'plugin'>;
export const searchIndexServiceRef: ServiceRef<
SearchIndexService,
'plugin',
'singleton'
>;
// (No @packageDocumentation comment for this package)
```
+6 -2
View File
@@ -37,7 +37,7 @@ export type SignalPayload<TMessage extends JsonObject = JsonObject> = {
export interface SignalService extends SignalsService {}
// @public @deprecated (undocumented)
export const signalService: ServiceRef<SignalsService, 'plugin'>;
export const signalService: ServiceRef<SignalsService, 'plugin', 'singleton'>;
// @public (undocumented)
export interface SignalsService {
@@ -52,7 +52,11 @@ export type SignalsServiceOptions = {
};
// @public (undocumented)
export const signalsServiceRef: ServiceRef<SignalsService, 'plugin'>;
export const signalsServiceRef: ServiceRef<
SignalsService,
'plugin',
'singleton'
>;
// (No @packageDocumentation comment for this package)
```