Registry API Refactoring (#144)

* Refactor core to use a flexible registry pattern

- Introduce a new registry mechanism for mailer providers
- Extract mailer provider enum to a separate file
- Implement dynamic mailer loading using a registry
- Update package dependencies and exports
- Improve modularity and extensibility of mailer implementation

* Refactor monitoring and billing services to use a flexible registry pattern

- Introduce a shared registry mechanism for dynamic service loading
- Replace static switch-based implementations with a registry-based approach
- Update instrumentation, CMS, and monitoring services to use the new registry
- Improve modularity and extensibility of service implementations
- Add Zod-based type-safe provider validation

* Simplify async registration in monitoring and billing services

- Remove unnecessary async wrappers for no-op registrations
- Update type definitions to support both async and sync registration functions
- Standardize registration approach for Paddle and Sentry providers

* Remove Tailwind package from packages where it is not being needed

* Remove Tailwind config references from pnpm-lock.yaml

* Update instrumentation registry to support dynamic monitoring providers

- Modify type definition to use NonNullable MonitoringProvider
- Import MonitoringProvider type from get-monitoring-provider
- Enhance type safety for instrumentation registration
This commit is contained in:
Giancarlo Buomprisco
2025-02-05 17:38:43 +07:00
committed by GitHub
parent 3140f0cf21
commit 4a47df81db
31 changed files with 414 additions and 287 deletions

View File

@@ -1,23 +1,25 @@
import { createRegistry } from '../registry';
import { Logger as LoggerInstance } from './logger';
const LOGGER = process.env.LOGGER ?? 'pino';
// Define the type for the logger provider. Currently supporting 'pino'.
type LoggerProvider = 'pino';
/*
* Logger
* By default, the logger is set to use Pino. To change the logger, update the import statement below.
* to your desired logger implementation.
const LOGGER = (process.env.LOGGER ?? 'pino') as LoggerProvider;
// Create a registry for logger implementations
const loggerRegistry = createRegistry<LoggerInstance, LoggerProvider>();
// Register the 'pino' logger implementation
loggerRegistry.register('pino', async () => {
const { Logger: PinoLogger } = await import('./impl/pino');
return PinoLogger;
});
/**
* @name getLogger
* @description Retrieves the logger implementation based on the LOGGER environment variable using the registry API.
*/
async function getLogger(): Promise<LoggerInstance> {
switch (LOGGER) {
case 'pino': {
const { Logger: PinoLogger } = await import('./impl/pino');
return PinoLogger;
}
default:
throw new Error(`Unknown logger: ${process.env.LOGGER}`);
}
export async function getLogger() {
return loggerRegistry.get(LOGGER);
}
export { getLogger };

View File

@@ -0,0 +1,108 @@
/**
* Implementation factory type
*/
export type ImplementationFactory<T> = () =>
| T
| Promise<T>;
/**
* Public API types with improved get method
*/
export interface Registry<T, Names extends string> {
register: (
name: Names,
factory: ImplementationFactory<T>,
) => Registry<T, Names>;
// Overloaded get method that infers return types based on input.
get: {
<K extends Names>(name: K): Promise<T>;
<K extends [Names, ...Names[]]>(
...names: K
): Promise<{ [P in keyof K]: T }>;
};
setup: (group?: string) => Promise<void>;
addSetup: (
group: string,
callback: () => Promise<void>,
) => Registry<T, Names>;
}
/**
* @name createRegistry
* @description Creates a new registry instance with the provided implementations.
* @returns A new registry instance.
*/
export function createRegistry<
T,
Names extends string = string,
>(): Registry<T, Names> {
const implementations = new Map<Names, ImplementationFactory<T>>();
const setupCallbacks = new Map<string, Array<() => Promise<void>>>();
const setupPromises = new Map<string, Promise<void>>();
const registry: Registry<T, Names> = {
register(name, factory) {
implementations.set(name, factory);
return registry;
},
// Updated get method overload that supports tuple inference
get: (async (...names: Names[]) => {
await registry.setup();
if (names.length === 1) {
return await getImplementation(names[0]!);
}
return await Promise.all(names.map((name) => getImplementation(name)));
}) as Registry<T, Names>['get'],
async setup(group?: string) {
if (group) {
if (!setupPromises.has(group)) {
const callbacks = setupCallbacks.get(group) ?? [];
setupPromises.set(
group,
Promise.all(callbacks.map((cb) => cb())).then(() => void 0),
);
}
return setupPromises.get(group);
}
const groups = Array.from(setupCallbacks.keys());
await Promise.all(groups.map((group) => registry.setup(group)));
},
addSetup(group, callback) {
if (!setupCallbacks.has(group)) {
setupCallbacks.set(group, []);
}
setupCallbacks.get(group)!.push(callback);
return registry;
},
};
async function getImplementation(name: Names) {
const factory = implementations.get(name);
if (!factory) {
throw new Error(`Implementation "${name}" not found`);
}
const implementation = await factory();
if (!implementation) {
throw new Error(`Implementation "${name}" is not available`);
}
return implementation;
}
return registry;
}