Merge pull request #15748 from backstage/mob/new-factories

backend-plugin-api: rework service factory definitions
This commit is contained in:
Patrik Oldsberg
2023-01-16 15:07:17 +01:00
committed by GitHub
28 changed files with 751 additions and 251 deletions
+3 -1
View File
@@ -152,7 +152,9 @@ export type HttpServerOptions = {
};
// @public (undocumented)
export const identityFactory: () => ServiceFactory<IdentityService>;
export const identityFactory: (
options?: IdentityFactoryOptions | undefined,
) => ServiceFactory<IdentityService>;
// @public
export type IdentityFactoryOptions = {
@@ -27,10 +27,10 @@ export const cacheFactory = createServiceFactory({
config: coreServices.config,
plugin: coreServices.pluginMetadata,
},
async factory({ config }) {
const cacheManager = CacheManager.fromConfig(config);
return async ({ plugin }) => {
return cacheManager.forPlugin(plugin.getId());
};
async createRootContext({ config }) {
return CacheManager.fromConfig(config);
},
async factory({ plugin }, manager) {
return manager.forPlugin(plugin.getId());
},
});
@@ -28,8 +28,8 @@ export const databaseFactory = createServiceFactory({
config: coreServices.config,
plugin: coreServices.pluginMetadata,
},
async factory({ config }) {
const databaseManager = config.getOptional('backend.database')
async createRootContext({ config }) {
return config.getOptional('backend.database')
? DatabaseManager.fromConfig(config)
: DatabaseManager.fromConfig(
new ConfigReader({
@@ -38,9 +38,8 @@ export const databaseFactory = createServiceFactory({
},
}),
);
return async ({ plugin }) => {
return databaseManager.forPlugin(plugin.getId());
};
},
async factory({ plugin }, databaseManager) {
return databaseManager.forPlugin(plugin.getId());
},
});
@@ -27,9 +27,6 @@ export const discoveryFactory = createServiceFactory({
config: coreServices.config,
},
async factory({ config }) {
const discovery = SingleHostDiscovery.fromConfig(config);
return async () => {
return discovery;
};
return SingleHostDiscovery.fromConfig(config);
},
});
@@ -27,16 +27,27 @@ describe('httpRouterFactory', () => {
ServiceFactory<HttpRouterService>,
{ scope: 'root' }
>;
const innerFactory = await factory.factory({ rootHttpRouter });
const handler1 = () => {};
const router1 = await innerFactory({ plugin: { getId: () => 'test1' } });
const router1 = await factory.factory(
{
rootHttpRouter,
plugin: { getId: () => 'test1' },
},
undefined,
);
router1.use(handler1);
expect(rootHttpRouter.use).toHaveBeenCalledTimes(1);
expect(rootHttpRouter.use).toHaveBeenCalledWith('/api/test1', handler1);
const handler2 = () => {};
const router2 = await innerFactory({ plugin: { getId: () => 'test2' } });
const router2 = await factory.factory(
{
rootHttpRouter,
plugin: { getId: () => 'test2' },
},
undefined,
);
router2.use(handler2);
expect(rootHttpRouter.use).toHaveBeenCalledTimes(2);
expect(rootHttpRouter.use).toHaveBeenCalledWith('/api/test2', handler2);
@@ -47,10 +58,15 @@ describe('httpRouterFactory', () => {
const factory = httpRouterFactory({
getPath: id => `/some/${id}/path`,
}) as Exclude<ServiceFactory<HttpRouterService>, { scope: 'root' }>;
const innerFactory = await factory.factory({ rootHttpRouter });
const handler1 = () => {};
const router1 = await innerFactory({ plugin: { getId: () => 'test1' } });
const router1 = await factory.factory(
{
rootHttpRouter,
plugin: { getId: () => 'test1' },
},
undefined,
);
router1.use(handler1);
expect(rootHttpRouter.use).toHaveBeenCalledTimes(1);
expect(rootHttpRouter.use).toHaveBeenCalledWith(
@@ -59,7 +75,13 @@ describe('httpRouterFactory', () => {
);
const handler2 = () => {};
const router2 = await innerFactory({ plugin: { getId: () => 'test2' } });
const router2 = await factory.factory(
{
rootHttpRouter,
plugin: { getId: () => 'test2' },
},
undefined,
);
router2.use(handler2);
expect(rootHttpRouter.use).toHaveBeenCalledTimes(2);
expect(rootHttpRouter.use).toHaveBeenCalledWith(
@@ -38,16 +38,13 @@ export const httpRouterFactory = createServiceFactory(
plugin: coreServices.pluginMetadata,
rootHttpRouter: coreServices.rootHttpRouter,
},
async factory({ rootHttpRouter }) {
async factory({ plugin, rootHttpRouter }) {
const getPath = options?.getPath ?? (id => `/api/${id}`);
return async ({ plugin }) => {
const path = getPath(plugin.getId());
return {
use(handler: Handler) {
rootHttpRouter.use(path, handler);
},
};
const path = getPath(plugin.getId());
return {
use(handler: Handler) {
rootHttpRouter.use(path, handler);
},
};
},
}),
@@ -34,17 +34,14 @@ export type IdentityFactoryOptions = {
};
/** @public */
export const identityFactory = createServiceFactory({
service: coreServices.identity,
deps: {
config: coreServices.config,
discovery: coreServices.discovery,
tokenManager: coreServices.tokenManager,
},
async factory({}, options?: IdentityFactoryOptions) {
return async ({ discovery }) => {
export const identityFactory = createServiceFactory(
(options?: IdentityFactoryOptions) => ({
service: coreServices.identity,
deps: {
discovery: coreServices.discovery,
},
async factory({ discovery }) {
return DefaultIdentityClient.create({ discovery, ...options });
};
},
});
},
}),
);
@@ -29,18 +29,16 @@ export const lifecycleFactory = createServiceFactory({
rootLifecycle: coreServices.rootLifecycle,
pluginMetadata: coreServices.pluginMetadata,
},
async factory({ rootLifecycle }) {
return async ({ logger, pluginMetadata }) => {
const plugin = pluginMetadata.getId();
return {
addShutdownHook(options: LifecycleServiceShutdownHook): void {
rootLifecycle.addShutdownHook({
...options,
async factory({ rootLifecycle, logger, pluginMetadata }) {
const plugin = pluginMetadata.getId();
return {
addShutdownHook(options: LifecycleServiceShutdownHook): void {
rootLifecycle.addShutdownHook({
...options,
logger: options.logger?.child({ plugin }) ?? logger,
});
},
};
logger: options.logger?.child({ plugin }) ?? logger,
});
},
};
},
});
@@ -26,9 +26,7 @@ export const loggerFactory = createServiceFactory({
rootLogger: coreServices.rootLogger,
plugin: coreServices.pluginMetadata,
},
async factory({ rootLogger }) {
return async ({ plugin }) => {
return rootLogger.child({ plugin: plugin.getId() });
};
factory({ rootLogger, plugin }) {
return rootLogger.child({ plugin: plugin.getId() });
},
});
@@ -28,12 +28,10 @@ export const permissionsFactory = createServiceFactory({
discovery: coreServices.discovery,
tokenManager: coreServices.tokenManager,
},
async factory({ config }) {
return async ({ discovery, tokenManager }) => {
return ServerPermissionClient.fromConfig(config, {
discovery,
tokenManager,
});
};
async factory({ config, discovery, tokenManager }) {
return ServerPermissionClient.fromConfig(config, {
discovery,
tokenManager,
});
},
});
@@ -29,13 +29,11 @@ export const schedulerFactory = createServiceFactory({
databaseManager: coreServices.database,
logger: coreServices.logger,
},
async factory() {
return async ({ plugin, databaseManager, logger }) => {
return TaskScheduler.forPlugin({
pluginId: plugin.getId(),
databaseManager,
logger: loggerToWinstonLogger(logger),
});
};
async factory({ plugin, databaseManager, logger }) {
return TaskScheduler.forPlugin({
pluginId: plugin.getId(),
databaseManager,
logger: loggerToWinstonLogger(logger),
});
},
});
@@ -0,0 +1,45 @@
/*
* Copyright 2022 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {
LoggerService,
ServiceFactory,
TokenManagerService,
} from '@backstage/backend-plugin-api';
import { ConfigReader } from '@backstage/config';
import { tokenManagerFactory } from './tokenManagerFactory';
describe('tokenManagerFactory', () => {
it('should create managers that can share tokens in development', async () => {
(process.env as { NODE_ENV?: string }).NODE_ENV = 'development';
const factory = tokenManagerFactory() as Exclude<
ServiceFactory<TokenManagerService>,
{ scope: 'root' }
>;
const deps = {
config: new ConfigReader({}),
logger: { warn() {} } as unknown as LoggerService,
};
const ctx = await factory.createRootContext?.(deps);
const manager1 = await factory.factory!(deps, ctx);
const manager2 = await factory.factory!(deps, ctx);
const { token } = await manager1.getToken();
await expect(manager2.authenticate(token)).resolves.toBeUndefined();
});
});
@@ -27,10 +27,12 @@ export const tokenManagerFactory = createServiceFactory({
config: coreServices.config,
logger: coreServices.rootLogger,
},
async factory({ config, logger }) {
const tokenManager = ServerTokenManager.fromConfig(config, {
createRootContext({ config, logger }) {
return ServerTokenManager.fromConfig(config, {
logger,
});
return async () => tokenManager;
},
async factory(_deps, tokenManager) {
return tokenManager;
},
});
@@ -27,12 +27,10 @@ export const urlReaderFactory = createServiceFactory({
config: coreServices.config,
logger: coreServices.logger,
},
async factory() {
return async ({ config, logger }) => {
return UrlReaders.default({
config,
logger: loggerToWinstonLogger(logger),
});
};
async factory({ config, logger }) {
return UrlReaders.default({
config,
logger: loggerToWinstonLogger(logger),
});
},
});
@@ -29,9 +29,7 @@ const sf1 = createServiceFactory({
service: ref1,
deps: {},
async factory() {
return async () => {
return { x: 1 };
};
return { x: 1 };
},
})();
@@ -61,7 +59,7 @@ const refDefault1 = createServiceRef<{ x: number }>({
service,
deps: {},
async factory() {
return async () => ({ x: 10 });
return { x: 10 };
},
})(),
});
@@ -73,7 +71,7 @@ const refDefault2a = createServiceRef<{ x: number }>({
service,
deps: {},
async factory() {
return async () => ({ x: 20 });
return { x: 20 };
},
}),
});
@@ -85,7 +83,7 @@ const refDefault2b = createServiceRef<{ x: number }>({
service,
deps: {},
async factory() {
return async () => ({ x: 220 });
return { x: 220 };
},
}),
});
@@ -144,7 +142,7 @@ describe('ServiceRegistry', () => {
service: ref1,
deps: { rootDep: ref2 },
factory: async ({ rootDep }) => {
return async () => ({ x: rootDep.x });
return { x: rootDep.x };
},
});
const registry = new ServiceRegistry([factory(), sf2]);
@@ -173,8 +171,8 @@ describe('ServiceRegistry', () => {
const factory = createServiceFactory({
service: ref,
deps: { meta: coreServices.pluginMetadata },
async factory() {
return async ({ meta }) => ({ pluginId: meta.getId() });
async factory({ meta }) {
return { pluginId: meta.getId() };
},
});
const registry = new ServiceRegistry([factory()]);
@@ -224,13 +222,11 @@ describe('ServiceRegistry', () => {
});
it('should only call each default factory loader once', async () => {
const factoryLoader = jest.fn(async (service: ServiceRef<void>) =>
const factoryLoader = jest.fn(async (service: ServiceRef<void, 'plugin'>) =>
createServiceFactory({
service,
deps: {},
async factory() {
return async () => {};
},
async factory() {},
}),
);
const ref = createServiceRef<void>({
@@ -247,10 +243,31 @@ describe('ServiceRegistry', () => {
});
it('should not call factory functions more than once', async () => {
const innerFactory = jest.fn(async () => {
return { x: 1 };
const createRootContext = jest.fn(async () => ({ x: 1 }));
const factory = jest.fn(async () => ({ x: 1 }));
const myFactory = createServiceFactory({
service: ref1,
deps: {},
createRootContext,
factory,
});
const factory = jest.fn(async () => innerFactory);
const registry = new ServiceRegistry([myFactory()]);
await Promise.all([
registry.get(ref1, 'catalog')!,
registry.get(ref1, 'catalog')!,
registry.get(ref1, 'catalog')!,
registry.get(ref1, 'scaffolder')!,
registry.get(ref1, 'scaffolder')!,
]);
expect(createRootContext).toHaveBeenCalledTimes(1);
expect(factory).toHaveBeenCalledTimes(2);
});
it('should not call factory functions more than once without root context', async () => {
const factory = jest.fn(async () => ({ x: 1 }));
const myFactory = createServiceFactory({
service: ref1,
deps: {},
@@ -267,8 +284,7 @@ describe('ServiceRegistry', () => {
registry.get(ref1, 'scaffolder')!,
]);
expect(factory).toHaveBeenCalledTimes(1);
expect(innerFactory).toHaveBeenCalledTimes(2);
expect(factory).toHaveBeenCalledTimes(2);
});
it('should throw if dependencies are not available', async () => {
@@ -296,9 +312,7 @@ describe('ServiceRegistry', () => {
const factoryA = createServiceFactory({
service: refA,
deps: { b: refB },
async factory() {
return async ({ b }) => b;
},
factory: async ({ b }) => b,
});
const factoryB = createServiceFactory({
@@ -320,15 +334,18 @@ describe('ServiceRegistry', () => {
const myFactory = createServiceFactory({
service: ref1,
deps: {},
factory() {
createRootContext() {
throw new Error('top-level error');
},
factory() {
throw new Error(`error in plugin`);
},
});
const registry = new ServiceRegistry([myFactory()]);
await expect(registry.get(ref1, 'catalog')).rejects.toThrow(
"Failed to instantiate service '1' because the top-level factory function threw an error, Error: top-level error",
"Failed to instantiate service '1' because createRootContext threw an error, Error: top-level error",
);
});
@@ -337,9 +354,7 @@ describe('ServiceRegistry', () => {
service: ref1,
deps: {},
async factory() {
return () => {
throw new Error(`error in plugin`);
};
throw new Error(`error in plugin`);
},
});
@@ -37,12 +37,14 @@ export class ServiceRegistry implements EnumerableServiceHolder {
readonly #implementations: Map<
ServiceFactory,
{
factoryFunc: Promise<
(deps: { [name in string]: unknown }) => Promise<unknown>
>;
context: Promise<unknown>;
byPlugin: Map<string, Promise<unknown>>;
}
>;
readonly #rootServiceImplementations = new Map<
ServiceFactory,
Promise<unknown>
>();
constructor(factories: Array<ServiceFactory<unknown>>) {
this.#providedFactories = new Map(factories.map(f => [f.service.id, f]));
@@ -56,15 +58,13 @@ export class ServiceRegistry implements EnumerableServiceHolder {
): Promise<ServiceFactory> | 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<
ServiceFactory<typeof coreServices.pluginMetadata.T>
>({
scope: 'plugin',
service: coreServices.pluginMetadata,
deps: {},
factory: async () => async () => ({
getId() {
return pluginId;
},
}),
factory: async () => ({ getId: () => pluginId }),
});
}
@@ -100,8 +100,6 @@ export class ServiceRegistry implements EnumerableServiceHolder {
return Promise.resolve(resolvedFactory);
}
#separateMapForTheRootService = new Map<ServiceFactory, Promise<unknown>>();
#checkForMissingDeps(factory: ServiceFactory, pluginId: string) {
const missingDeps = Object.values(factory.deps).filter(ref => {
if (ref.id === coreServices.pluginMetadata.id) {
@@ -129,7 +127,7 @@ export class ServiceRegistry implements EnumerableServiceHolder {
get<T>(ref: ServiceRef<T>, pluginId: string): Promise<T> | undefined {
return this.#resolveFactory(ref, pluginId)?.then(factory => {
if (factory.scope === 'root') {
let existing = this.#separateMapForTheRootService.get(factory);
let existing = this.#rootServiceImplementations.get(factory);
if (!existing) {
this.#checkForMissingDeps(factory, pluginId);
const rootDeps = new Array<Promise<[name: string, impl: unknown]>>();
@@ -147,7 +145,7 @@ export class ServiceRegistry implements EnumerableServiceHolder {
existing = Promise.all(rootDeps).then(entries =>
factory.factory(Object.fromEntries(entries)),
);
this.#separateMapForTheRootService.set(factory, existing);
this.#rootServiceImplementations.set(factory, existing);
}
return existing as Promise<T>;
}
@@ -165,12 +163,14 @@ export class ServiceRegistry implements EnumerableServiceHolder {
}
implementation = {
factoryFunc: Promise.all(rootDeps)
.then(entries => factory.factory(Object.fromEntries(entries)))
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 the top-level factory function threw an error, ${cause}`,
`Failed to instantiate service '${ref.id}' because createRootContext threw an error, ${cause}`,
);
}),
byPlugin: new Map(),
@@ -188,10 +188,10 @@ export class ServiceRegistry implements EnumerableServiceHolder {
allDeps.push(target.then(impl => [name, impl]));
}
result = implementation.factoryFunc
.then(func =>
result = implementation.context
.then(context =>
Promise.all(allDeps).then(entries =>
func(Object.fromEntries(entries)),
factory.factory(Object.fromEntries(entries), context),
),
)
.catch(error => {
@@ -53,7 +53,7 @@ describe('createSpecializedBackend', () => {
createServiceFactory({
service: coreServices.pluginMetadata,
deps: {},
factory: async () => async () => ({ getId: () => 'test' }),
factory: async () => ({ getId: () => 'test' }),
}),
],
}),
@@ -63,7 +63,7 @@ describe('createBackend', () => {
createServiceFactory({
service: coreServices.pluginMetadata,
deps: {},
factory: async () => async () => ({ getId: () => 'test' }),
factory: async () => ({ getId: () => 'test' }),
}),
],
}),
+118 -33
View File
@@ -126,22 +126,86 @@ export function createExtensionPoint<T>(
config: ExtensionPointConfig,
): ExtensionPoint<T>;
// @public (undocumented)
// @public
export function createServiceFactory<
TService,
TScope extends 'root' | 'plugin',
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
TOpts extends [options?: object] = [],
TOpts extends object | undefined = undefined,
>(
config: RootServiceFactoryConfig<TService, TImpl, TDeps>,
): () => ServiceFactory<TService>;
// @public
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
TOpts extends object | undefined = undefined,
>(
config: (options?: TOpts) => RootServiceFactoryConfig<TService, TImpl, TDeps>,
): (options?: TOpts) => ServiceFactory<TService>;
// @public
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
TOpts extends object | undefined = undefined,
>(
config: (options: TOpts) => RootServiceFactoryConfig<TService, TImpl, TDeps>,
): (options: TOpts) => ServiceFactory<TService>;
// @public
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
TContext = undefined,
TOpts extends object | undefined = undefined,
>(
config: PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>,
): () => ServiceFactory<TService>;
// @public
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
TContext = undefined,
TOpts extends object | undefined = undefined,
>(
config: (
options?: TOpts,
) => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>,
): (options?: TOpts) => ServiceFactory<TService>;
// @public
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
TContext = undefined,
TOpts extends object | undefined = undefined,
>(
config:
| ServiceFactoryConfig<TService, TScope, TImpl, TDeps>
| PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>
| ((
...options: TOpts
) => ServiceFactoryConfig<TService, TScope, TImpl, TDeps>),
): (...params: TOpts) => ServiceFactory<TService>;
options: TOpts,
) => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>),
): (options: TOpts) => ServiceFactory<TService>;
// @public
export function createServiceRef<TService>(
@@ -229,6 +293,30 @@ export interface PluginMetadataService {
getId(): string;
}
// @public (undocumented)
export interface PluginServiceFactoryConfig<
TService,
TContext,
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
> {
// (undocumented)
createRootContext?(
deps: ServiceRefsToInstances<TDeps, 'root'>,
): TContext | Promise<TContext>;
// (undocumented)
deps: TDeps;
// (undocumented)
factory(
deps: ServiceRefsToInstances<TDeps>,
context: TContext,
): TImpl | Promise<TImpl>;
// (undocumented)
service: ServiceRef<TService, 'plugin'>;
}
// @public
export type ReadTreeOptions = {
filter?(
@@ -284,6 +372,22 @@ export interface RootLifecycleService extends LifecycleService {}
// @public (undocumented)
export interface RootLoggerService extends LoggerService {}
// @public (undocumented)
export interface RootServiceFactoryConfig<
TService,
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
> {
// (undocumented)
deps: TDeps;
// (undocumented)
factory(deps: ServiceRefsToInstances<TDeps, 'root'>): TImpl | Promise<TImpl>;
// (undocumented)
service: ServiceRef<TService, 'root'>;
}
// @public (undocumented)
export interface SchedulerService extends PluginTaskScheduler {}
@@ -323,36 +427,17 @@ export type ServiceFactory<TService = unknown> =
deps: {
[key in string]: ServiceRef<unknown>;
};
factory(deps: {
createRootContext?(deps: {
[key in string]: unknown;
}): Promise<
(deps: {
}): Promise<unknown>;
factory(
deps: {
[key in string]: unknown;
}) => Promise<TService>
>;
},
context: unknown,
): Promise<TService>;
};
// @public (undocumented)
export interface ServiceFactoryConfig<
TService,
TScope extends 'root' | 'plugin',
TImpl extends TService,
TDeps extends {
[name in string]: ServiceRef<unknown>;
},
> {
// (undocumented)
deps: TDeps;
// (undocumented)
factory(
deps: ServiceRefsToInstances<TDeps, 'root'>,
): TScope extends 'root'
? Promise<TImpl>
: Promise<(deps: ServiceRefsToInstances<TDeps>) => Promise<TImpl>>;
// (undocumented)
service: ServiceRef<TService, TScope>;
}
// @public
export type ServiceFactoryOrFunction<TService = unknown> =
| ServiceFactory<TService>
@@ -19,7 +19,8 @@ export type {
ServiceRefConfig,
TypesToServiceRef,
ServiceFactory,
ServiceFactoryConfig,
PluginServiceFactoryConfig,
RootServiceFactoryConfig,
ServiceFactoryOrFunction,
} from './types';
export { createServiceRef, createServiceFactory } from './types';
@@ -20,13 +20,19 @@ const ref = createServiceRef<string>({ id: 'x' });
const rootDep = createServiceRef<number>({ id: 'y', scope: 'root' });
const pluginDep = createServiceRef<boolean>({ id: 'z' });
interface TestOptions {
x: number;
}
function unused(..._any: any[]) {}
describe('createServiceFactory', () => {
it('should create a meta factory with no options', () => {
it('should create a sync factory with no options', () => {
const metaFactory = createServiceFactory({
service: ref,
deps: {},
async factory(_deps) {
return async () => 'x';
createRootContext() {},
factory(_deps) {
return 'x';
},
});
expect(metaFactory).toEqual(expect.any(Function));
@@ -45,12 +51,62 @@ describe('createServiceFactory', () => {
metaFactory();
});
it('should create a meta factory with optional options', () => {
it('should create a sync root factory with no options', () => {
const metaFactory = createServiceFactory({
service: rootDep,
deps: {},
factory(_deps) {
return 0;
},
});
expect(metaFactory).toEqual(expect.any(Function));
expect(metaFactory().service).toBe(rootDep);
// @ts-expect-error
metaFactory('string');
// @ts-expect-error
metaFactory({});
// @ts-expect-error
metaFactory({ x: 1 });
// @ts-expect-error
metaFactory(null);
// @ts-expect-error
metaFactory(undefined);
metaFactory();
});
it('should create a factory with no options', () => {
const metaFactory = createServiceFactory({
service: ref,
deps: {},
async createRootContext() {},
async factory(_deps) {
return 'x';
},
});
expect(metaFactory).toEqual(expect.any(Function));
expect(metaFactory().service).toBe(ref);
// @ts-expect-error
metaFactory('string');
// @ts-expect-error
metaFactory({});
// @ts-expect-error
metaFactory({ x: 1 });
// @ts-expect-error
metaFactory(null);
// @ts-expect-error
metaFactory(undefined);
metaFactory();
});
it('should create a factory with optional options', () => {
const metaFactory = createServiceFactory((_opts?: { x: number }) => ({
service: ref,
deps: {},
async createRootContext() {},
async factory() {
return async () => 'x';
return 'x';
},
}));
expect(metaFactory).toEqual(expect.any(Function));
@@ -68,12 +124,13 @@ describe('createServiceFactory', () => {
metaFactory();
});
it('should create a meta factory with required options', () => {
it('should create a factory with required options', () => {
const metaFactory = createServiceFactory((_opts: { x: number }) => ({
service: ref,
deps: {},
async createRootContext() {},
async factory() {
return async () => 'x';
return 'x';
},
}));
expect(metaFactory).toEqual(expect.any(Function));
@@ -93,15 +150,13 @@ describe('createServiceFactory', () => {
metaFactory();
});
it('should create a meta factory with optional options as interface', () => {
interface TestOptions {
x: number;
}
it('should create a factory with optional options as interface', () => {
const metaFactory = createServiceFactory((_opts?: TestOptions) => ({
service: ref,
deps: {},
async createRootContext() {},
async factory() {
return async () => 'x';
return 'x';
},
}));
expect(metaFactory).toEqual(expect.any(Function));
@@ -119,15 +174,13 @@ describe('createServiceFactory', () => {
metaFactory();
});
it('should create a meta factory with required options as interface', () => {
interface TestOptions {
x: number;
}
it('should create a factory with required options as interface', () => {
const metaFactory = createServiceFactory((_opts: TestOptions) => ({
service: ref,
deps: {},
async createRootContext() {},
async factory() {
return async () => 'x';
return 'x';
},
}));
expect(metaFactory).toEqual(expect.any(Function));
@@ -147,15 +200,9 @@ describe('createServiceFactory', () => {
metaFactory();
});
it('should create factory with required options and dependencies', () => {
interface TestOptions {
x: number;
}
function unused(..._any: any[]) {}
const metaFactory = createServiceFactory((_opts: TestOptions) => ({
service: ref,
it('should create root scoped factory with dependencies', () => {
const metaFactory = createServiceFactory({
service: createServiceRef({ id: 'foo', scope: 'root' }),
deps: {
root: rootDep,
plugin: pluginDep,
@@ -164,13 +211,143 @@ describe('createServiceFactory', () => {
const root1: number = root;
// @ts-expect-error
const root2: string = root;
return async ({ plugin }) => {
const plugin3: boolean = plugin;
// @ts-expect-error
const plugin4: number = plugin;
unused(root1, root2, plugin3, plugin4);
return 'x';
};
unused(root1, root2);
return 0;
},
});
expect(metaFactory).toEqual(expect.any(Function));
// @ts-expect-error
metaFactory({});
// @ts-expect-error
metaFactory(null);
// @ts-expect-error
metaFactory(undefined);
metaFactory();
});
it('should create root scoped factory with dependencies and optional options', () => {
const metaFactory = createServiceFactory((_options?: TestOptions) => ({
service: createServiceRef({ id: 'foo', scope: 'root' }),
deps: {
root: rootDep,
plugin: pluginDep,
},
async factory({ root }) {
const root1: number = root;
// @ts-expect-error
const root2: string = root;
unused(root1, root2);
return 0;
},
}));
expect(metaFactory).toEqual(expect.any(Function));
// @ts-expect-error
metaFactory('string');
// @ts-expect-error
metaFactory({});
metaFactory({ x: 1 });
// @ts-expect-error
metaFactory({ x: 1, y: 2 });
// @ts-expect-error
metaFactory(null);
metaFactory(undefined);
metaFactory();
});
it('should create factory with dependencies', () => {
const metaFactory = createServiceFactory({
service: createServiceRef({ id: 'derp' }),
deps: {
root: rootDep,
plugin: pluginDep,
},
async createRootContext({ root }) {
const root1: number = root;
// @ts-expect-error
const root2: string = root;
unused(root1, root2);
return { root };
},
async factory({ plugin, root: rootB }, { root }) {
const root1: number = root;
// @ts-expect-error
const root2: string = root;
const root3: number = rootB;
// @ts-expect-error
const root4: string = rootB;
const plugin3: boolean = plugin;
// @ts-expect-error
const plugin4: number = plugin;
unused(root1, root2, root3, root4, plugin3, plugin4);
return 'x';
},
});
expect(metaFactory).toEqual(expect.any(Function));
// @ts-expect-error
metaFactory({});
// @ts-expect-error
metaFactory(null);
// @ts-expect-error
metaFactory(undefined);
metaFactory();
});
it('should create factory with dependencies with optional derpFactory', () => {
const metaFactory = createServiceFactory({
service: createServiceRef({ id: 'derp' }),
deps: {
root: rootDep,
plugin: pluginDep,
},
async factory({ root, plugin }) {
const root1: number = root;
// @ts-expect-error
const root2: string = root;
const plugin3: boolean = plugin;
// @ts-expect-error
const plugin4: number = plugin;
unused(root1, root2, plugin3, plugin4);
return 'x';
},
});
expect(metaFactory).toEqual(expect.any(Function));
// @ts-expect-error
metaFactory({});
// @ts-expect-error
metaFactory(null);
// @ts-expect-error
metaFactory(undefined);
metaFactory();
});
it('should create factory with required options and dependencies', () => {
const metaFactory = createServiceFactory((_opts: TestOptions) => ({
service: ref,
deps: {
root: rootDep,
plugin: pluginDep,
},
async createRootContext({ root }) {
const root1: number = root;
// @ts-expect-error
const root2: string = root;
unused(root1, root2);
return { root };
},
async factory({ plugin }, { root }) {
const root1: number = root;
// @ts-expect-error
const root2: string = root;
const plugin3: boolean = plugin;
// @ts-expect-error
const plugin4: number = plugin;
unused(root1, root2, plugin3, plugin4);
return 'x';
},
}));
expect(metaFactory).toEqual(expect.any(Function));
@@ -191,29 +368,28 @@ describe('createServiceFactory', () => {
});
it('should create factory with optional options and dependencies', () => {
interface TestOptions {
x: number;
}
function unused(..._any: any[]) {}
const metaFactory = createServiceFactory((_opts?: TestOptions) => ({
service: ref,
deps: {
root: rootDep,
plugin: pluginDep,
},
async factory({ root }) {
async createRootContext({ root }) {
const root1: number = root;
// @ts-expect-error
const root2: string = root;
return async ({ plugin }) => {
const plugin3: boolean = plugin;
// @ts-expect-error
const plugin4: number = plugin;
unused(root1, root2, plugin3, plugin4);
return 'x';
};
unused(root1, root2);
return { root };
},
async factory({ plugin }, { root }) {
const root1: number = root;
// @ts-expect-error
const root2: string = root;
const plugin3: boolean = plugin;
// @ts-expect-error
const plugin4: number = plugin;
unused(root1, root2, plugin3, plugin4);
return 'x';
},
}));
expect(metaFactory).toEqual(expect.any(Function));
@@ -64,9 +64,11 @@ export type ServiceFactory<TService = unknown> =
scope: 'plugin';
service: ServiceRef<TService, 'plugin'>;
deps: { [key in string]: ServiceRef<unknown> };
factory(deps: { [key in string]: unknown }): Promise<
(deps: { [key in string]: unknown }) => Promise<TService>
>;
createRootContext?(deps: { [key in string]: unknown }): Promise<unknown>;
factory(
deps: { [key in string]: unknown },
context: unknown,
): Promise<TService>;
};
/**
@@ -135,46 +137,157 @@ type ServiceRefsToInstances<
};
/** @public */
export interface ServiceFactoryConfig<
export interface RootServiceFactoryConfig<
TService,
TScope extends 'root' | 'plugin',
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
> {
service: ServiceRef<TService, TScope>;
service: ServiceRef<TService, 'root'>;
deps: TDeps;
factory(
factory(deps: ServiceRefsToInstances<TDeps, 'root'>): TImpl | Promise<TImpl>;
}
/** @public */
export interface PluginServiceFactoryConfig<
TService,
TContext,
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
> {
service: ServiceRef<TService, 'plugin'>;
deps: TDeps;
createRootContext?(
deps: ServiceRefsToInstances<TDeps, 'root'>,
): TScope extends 'root'
? Promise<TImpl>
: Promise<(deps: ServiceRefsToInstances<TDeps>) => Promise<TImpl>>;
): TContext | Promise<TContext>;
factory(
deps: ServiceRefsToInstances<TDeps>,
context: TContext,
): TImpl | Promise<TImpl>;
}
/**
* Creates a root scoped service factory without options.
*
* @public
* @param config - The service factory configuration.
*/
export function createServiceFactory<
TService,
TScope extends 'root' | 'plugin',
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TOpts extends [options?: object] = [],
TOpts extends object | undefined = undefined,
>(
config: RootServiceFactoryConfig<TService, TImpl, TDeps>,
): () => ServiceFactory<TService>;
/**
* Creates a root scoped service factory with optional options.
*
* @public
* @param config - The service factory configuration.
*/
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TOpts extends object | undefined = undefined,
>(
config: (options?: TOpts) => RootServiceFactoryConfig<TService, TImpl, TDeps>,
): (options?: TOpts) => ServiceFactory<TService>;
/**
* Creates a root scoped service factory with required options.
*
* @public
* @param config - The service factory configuration.
*/
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TOpts extends object | undefined = undefined,
>(
config: (options: TOpts) => RootServiceFactoryConfig<TService, TImpl, TDeps>,
): (options: TOpts) => ServiceFactory<TService>;
/**
* Creates a plugin scoped service factory without options.
*
* @public
* @param config - The service factory configuration.
*/
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TContext = undefined,
TOpts extends object | undefined = undefined,
>(
config: PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>,
): () => ServiceFactory<TService>;
/**
* Creates a plugin scoped service factory with optional options.
*
* @public
* @param config - The service factory configuration.
*/
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TContext = undefined,
TOpts extends object | undefined = undefined,
>(
config: (
options?: TOpts,
) => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>,
): (options?: TOpts) => ServiceFactory<TService>;
/**
* Creates a plugin scoped service factory with required options.
*
* @public
* @param config - The service factory configuration.
*/
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TContext = undefined,
TOpts extends object | undefined = undefined,
>(
config:
| ServiceFactoryConfig<TService, TScope, TImpl, TDeps>
| PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>
| ((
...options: TOpts
) => ServiceFactoryConfig<TService, TScope, TImpl, TDeps>),
): (...params: TOpts) => ServiceFactory<TService> {
if (typeof config === 'function') {
return (...opts: TOpts) => {
const c = config(...opts);
return { ...c, scope: c.service.scope } as ServiceFactory<TService>;
};
}
return () =>
({
...config,
scope: config.service.scope,
} as ServiceFactory<TService>);
options: TOpts,
) => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>),
): (options: TOpts) => ServiceFactory<TService>;
export function createServiceFactory<
TService,
TImpl extends TService,
TDeps extends { [name in string]: ServiceRef<unknown> },
TContext,
TOpts extends object | undefined = undefined,
>(
config:
| RootServiceFactoryConfig<TService, TImpl, TDeps>
| PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>
| ((options: TOpts) => RootServiceFactoryConfig<TService, TImpl, TDeps>)
| ((
options: TOpts,
) => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>)
| (() => RootServiceFactoryConfig<TService, TImpl, TDeps>)
| (() => PluginServiceFactoryConfig<TService, TContext, TImpl, TDeps>),
): (options: TOpts) => ServiceFactory<TService> {
const configCallback = typeof config === 'function' ? config : () => config;
return (options: TOpts) => {
const c = configCallback(options);
return {
...c,
...('createRootContext' in c
? {
createRootContext: async (deps: TDeps) =>
c?.createRootContext?.(deps),
}
: {}),
factory: async (deps: TDeps, ctx: TContext) => c.factory(deps, ctx),
scope: c.service.scope,
} as ServiceFactory<TService>;
};
}
@@ -35,8 +35,6 @@ export const mockTokenManagerFactory = createServiceFactory({
service: coreServices.tokenManager,
deps: {},
async factory() {
return async () => {
return new TokenManagerMock();
};
return new TokenManagerMock();
},
});
@@ -106,7 +106,7 @@ describe('TestBackend', () => {
deps: {},
service: testRef,
factory: async () => {
return async () => testFn;
return testFn;
},
});
@@ -168,7 +168,7 @@ export async function startTestBackend<
backend: { baseUrl: `http://localhost:${port}`, listen: { port } },
}),
);
return async () => discovery;
return discovery;
},
});
@@ -179,13 +179,13 @@ export async function startTestBackend<
const [ref, impl] = serviceDef;
if (ref.scope === 'plugin') {
return createServiceFactory({
service: ref,
service: ref as ServiceRef<unknown, 'plugin'>,
deps: {},
factory: async () => async () => impl,
factory: async () => impl,
})();
}
return createServiceFactory({
service: ref,
service: ref as ServiceRef<unknown, 'root'>,
deps: {},
factory: async () => impl,
})();