import type { ServiceConstructor, ServiceUid } from './types'; const LOG_PREFIX = '[lightweight-sdk:service-registry]'; /** * Central singleton registry that holds the single shared instance of every * service registered via {@link RegisterService}, keyed by the uid the * developer assigned at registration time. Prefer the ready-to-use * {@link services} constant over calling {@link ServiceRepository.getInstance} * directly. */ export class ServiceRepository { private static instance: ServiceRepository | undefined; private instances = new Map(); /** Private to enforce the singleton pattern — use {@link ServiceRepository.getInstance} or {@link services}. */ private constructor() {} /** * Returns the single shared instance of the repository, creating it on first access. * @returns the singleton {@link ServiceRepository} */ public static getInstance(): ServiceRepository { if (ServiceRepository.instance === undefined) { ServiceRepository.instance = new ServiceRepository(); } return ServiceRepository.instance; } /** * Clears every registered service. The singleton instance itself is kept * (not discarded), so the {@link services} constant stays valid across * resets. Intended for resetting state between test cases — application * code should not call this. */ public static resetForTesting(): void { ServiceRepository.getInstance().instances.clear(); } /** * Instantiates `target` (its constructor must take no arguments) and * registers the resulting instance under `uid`. Called by * {@link RegisterService} — most callers should use that decorator * instead of calling this method directly. * @param uid the unique identifier the caller assigns to this service * @param target the service class to instantiate and register * @throws {Error} if `uid` is already registered */ public register(uid: ServiceUid, target: ServiceConstructor): void { if (this.instances.has(uid)) { throw new Error(`Service uid "${uid}" is already registered.`); } const instance = new target(); this.instances.set(uid, instance); console.debug(`${LOG_PREFIX} Registered service "${uid}" (${target.name}).`); } /** * Resolves the instance registered under `uid`. * @param uid the service's unique identifier, as passed to {@link RegisterService} * @returns the instance registered under `uid` * @throws {Error} if no service is registered under `uid` * @example * ```ts * const logger = services.get(LOGGER_UID); * ``` */ public get(uid: ServiceUid): T { if (!this.instances.has(uid)) { throw new Error(`No service registered with uid "${uid}". Did you forget @RegisterService?`); } return this.instances.get(uid) as T; } /** * Checks whether a service is currently registered under `uid`. * @param uid the unique identifier to check * @returns `true` if a service is registered under `uid` */ public has(uid: ServiceUid): boolean { return this.instances.has(uid); } } /** * Ready-to-use singleton instance of {@link ServiceRepository}. Import this * wherever a service needs to be registered or resolved, instead of calling * {@link ServiceRepository.getInstance} directly. * @example * ```ts * import { services } from '@lightweight-sdk/service-registry'; * * const logger = services.get(LOGGER_UID); * ``` */ export const services = ServiceRepository.getInstance();