backend-{plugin,app}-api: introduce service scopes and update registry to support

Co-authored-by: Fredrik Adelöw <freben@gmail.com>
Co-authored-by: blam <ben@blam.sh>
Co-authored-by: Johan Haals <johan.haals@gmail.com>
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2022-09-08 15:46:16 +02:00
parent 541ea937d2
commit 229a1fa927
4 changed files with 284 additions and 194 deletions
@@ -19,63 +19,84 @@
*
* @public
*/
export type ServiceRef<T> = {
export type ServiceRef<
TService,
TScope extends 'root' | 'plugin' = 'root' | 'plugin',
> = {
id: string;
/**
* This determines the scope at which this service is available.
*
* Root scoped services are available to all other services but
* may only depend on other root scoped services.
*
* Plugin scoped services are only available to other plugin scoped
* services but may depend on all other services.
*/
scope: TScope;
/**
* Utility for getting the type of the service, using `typeof serviceRef.T`.
* Attempting to actually read this value will result in an exception.
*/
T: T;
T: TService;
toString(): string;
$$ref: 'service';
};
/**
* @internal
*/
export type InternalServiceRef<T> = ServiceRef<T> & {
/**
* The default factory that will be used to create service
* instances if no other factory is provided.
*/
__defaultFactory?: (
service: ServiceRef<T>,
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
};
/** @public */
export type TypesToServiceRef<T> = { [key in keyof T]: ServiceRef<T[key]> };
/** @public */
export type DepsToDepFactories<T> = {
[key in keyof T]: (pluginId: string) => Promise<T[key]>;
};
/** @public */
export type FactoryFunc<Impl> = (pluginId: string) => Promise<Impl>;
/** @public */
export type ServiceFactory<TService = unknown> = {
service: ServiceRef<TService>;
deps: { [key in string]: ServiceRef<unknown> };
factory(deps: { [key in string]: unknown }): Promise<FactoryFunc<TService>>;
};
export type ServiceFactory<TService = unknown> =
| {
// This scope prop is needed in addition to the service ref, as TypeScript
// can't properly discriminate the two factory types otherwise.
scope: 'root';
service: ServiceRef<TService, 'root'>;
deps: { [key in string]: ServiceRef<unknown> };
factory(deps: { [key in string]: unknown }): Promise<TService>;
}
| {
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>
>;
};
/**
* @public
*/
export function createServiceRef<T>(options: {
id: string;
scope?: 'plugin';
defaultFactory?: (
service: ServiceRef<T>,
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
}): ServiceRef<T> {
const { id, defaultFactory } = options;
}): ServiceRef<T, 'plugin'>;
export function createServiceRef<T>(options: {
id: string;
scope: 'root';
defaultFactory?: (
service: ServiceRef<T>,
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
}): ServiceRef<T, 'root'>;
export function createServiceRef<T>(options: {
id: string;
scope?: 'plugin' | 'root';
defaultFactory?: (
service: ServiceRef<T>,
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
}): ServiceRef<T, 'plugin' | 'root'> {
const { id, scope = 'plugin', defaultFactory } = options;
return {
id,
scope,
get T(): T {
throw new Error(`tried to read ServiceRef.T of ${this}`);
},
@@ -84,32 +105,58 @@ export function createServiceRef<T>(options: {
},
$$ref: 'service', // TODO: declare
__defaultFactory: defaultFactory,
} as InternalServiceRef<T>;
} as ServiceRef<T, typeof scope> & {
__defaultFactory?: (
service: ServiceRef<T>,
) => Promise<ServiceFactory<T> | (() => ServiceFactory<T>)>;
};
}
type OnlyRootScopeDependencies<
TDeps extends { [key in string]: ServiceRef<unknown> },
> = Pick<
TDeps,
{
[name in keyof TDeps]: TDeps[name]['scope'] extends 'root' ? name : never;
}[keyof TDeps]
>;
type DependencyRefsToInstances<
T extends { [key in string]: ServiceRef<unknown> },
> = {
[key in keyof T]: T[key] extends ServiceRef<infer TImpl> ? TImpl : never;
};
/**
* @public
*/
export function createServiceFactory<
TService,
TScope extends 'root' | 'plugin',
TImpl extends TService,
TDeps extends { [name in string]: unknown },
TDeps extends { [name in string]: ServiceRef<unknown> },
TOpts extends { [name in string]: unknown } | undefined = undefined,
>(factory: {
service: ServiceRef<TService>;
deps: TypesToServiceRef<TDeps>;
>(config: {
service: ServiceRef<TService, TScope>;
deps: TDeps;
factory(
deps: DepsToDepFactories<TDeps>,
deps: DependencyRefsToInstances<OnlyRootScopeDependencies<TDeps>>,
options: TOpts,
): Promise<FactoryFunc<TImpl>>;
): TScope extends 'root'
? Promise<TImpl>
: Promise<(deps: DependencyRefsToInstances<TDeps>) => Promise<TImpl>>;
}): undefined extends TOpts
? (options?: TOpts) => ServiceFactory<TService>
: (options: TOpts) => ServiceFactory<TService> {
return (options?: TOpts) => ({
service: factory.service,
deps: factory.deps,
factory(deps: DepsToDepFactories<TDeps>) {
return factory.factory(deps, options!);
},
});
return (options?: TOpts) =>
({
scope: config.service.scope,
service: config.service,
deps: config.deps,
factory(
deps: DependencyRefsToInstances<OnlyRootScopeDependencies<TDeps>>,
) {
return config.factory(deps, options!);
},
} as ServiceFactory<TService>);
}