/** * @description 定义创建具名单例项时使用的选项。 * * `value` 的返回值会被原样缓存,因此既可以是普通值,也可以是 `Promise`。 */ export interface SingletonOptions { name: Name value: () => Value } /** * @description 表示一个具名且按需初始化的单例项。 * * 该类会缓存工厂函数的首次返回值,并在后续访问时原样复用它。因此 `Value` 既可以是普通值, * 也可以是 `Promise`;若工厂返回的是 `Promise`,则缓存的是同一个 `Promise` 实例。 * * 调用方既可以通过 `getValue()` 原样读取缓存值,也可以通过 `getValueAsync()` 统一以 * 异步方式访问最终结果;若需要重新初始化,则可调用 `reset()` 清空缓存。 */ export class SingletonItem { private name: Name private value: () => Value private hasCache = false private cache: Value | undefined = undefined /** * @description 使用名称和值工厂创建单例项。 */ constructor(options: SingletonOptions) { this.name = options.name this.value = options.value } /** * @description 获取该单例项的名称。 */ getName(): Name { return this.name } /** * @description 获取该单例项的值,并在首次访问时完成初始化。 * * 该方法不会主动展开 `Promise`;若工厂返回的是 `Promise`,调用方应自行处理 `await`。 */ getValue(): Value { if (this.hasCache === false) { this.cache = this.value() this.hasCache = true } return this.cache as Value } /** * @description 以异步方式获取该单例项的值。 * * 若缓存值本身是 `Promise`,则会等待该 `Promise`;若缓存值是普通值, * 则会将其包装为已完成的 `Promise`。 * 该方法只保证异步访问最终结果,不保证返回同一个 `Promise` 实例。 * 若工厂函数抛出错误,则会返回 rejected promise。 */ async getValueAsync(): Promise> { return await Promise.resolve(this.getValue()) } /** * @description 重置该单例项的缓存。 * * 重置后,下次调用 `getValue()` 或 `getValueAsync()` 时会重新执行工厂函数。 */ reset(): this { this.cache = undefined this.hasCache = false return this } } /** * @description 创建一个具名单例项实例。 * * 创建后的单例项会原样缓存工厂函数的返回值,因此既可用于同步值,也可用于 `Promise` 值。 * * @example * ``` * // Scenario 1: sync values keep a stable name and a cached result. * let calls = 0 * const item = singletonItem("token", () => { * calls += 1 * return "secret" * }) * * // Expect: "token" * const example1 = item.getName() * // Expect: true * const example2 = item.getValue() === item.getValue() * // Expect: 1 * const example3 = calls * * // Scenario 2: promise values stay cached, and async access reads the resolved result. * let asyncCalls = 0 * const asyncItem = singletonItem("profile", async () => { * asyncCalls += 1 * return { enabled: true } * }) * const example4 = asyncItem.getValue() * const example5 = asyncItem.getValue() * const example6 = await asyncItem.getValueAsync() * * // Expect: true * const example7 = example4 === example5 * // Expect: true * const example8 = example6.enabled * // Expect: 1 * const example9 = asyncCalls * * // Scenario 3: reset clears the cache and forces re-initialization. * item.reset() * const example10 = item.getValue() * * // Expect: "secret" * const example11 = example10 * // Expect: 2 * const example12 = calls * ``` */ export const singletonItem = ( name: Name, value: () => Value, ): SingletonItem => { return new SingletonItem({ name, value }) } type TupleToUnion = T extends unknown[] ? T[number] : never type GetName = Items extends [] ? [] : Items extends [infer x, ...infer xs] ? x extends SingletonItem ? [Name, ...GetName] : never : never type GetValue = Items extends [] ? never : Items extends [infer x, ...infer xs] ? x extends SingletonItem ? name extends Name ? Name extends name ? value : GetValue : GetValue : never : never /** * @description 表示一个可按名称检索单例值的集合。 * * 集合不会改变单例项的值类型;若某个单例项缓存的是 `Promise`,则按名称读取时 * 也会返回该 `Promise`。 * * 调用方既可以通过 `getItem()` 原样读取,也可以通过 `getItemAsync()` 统一 * 以异步方式访问最终结果;若需要让集合中的所有单例项重新初始化,则可调用 `reset()`。 */ export class SingletonCollection>> { private map: Map> /** * @description 使用一组单例项创建集合。 */ constructor(items: [...Item]) { this.map = new Map() items.forEach((item) => this.map.set(item.getName(), item)) } /** * @description 按名称获取集合中的单例值。 * * 该方法会原样返回对应单例项的缓存值;若该值是 `Promise`,调用方应自行处理 `await`。 * * @throws 当名称不存在于集合中时抛出错误。 */ getItem>>(name: Name): GetValue { const item = this.map.get(name) if (item === undefined) { throw new Error(`GlobalService: item ${String(name)} not found`) } return item.getValue() as GetValue } /** * @description 以异步方式按名称获取集合中的单例值。 * * 若对应值本身是 `Promise`,则会等待该 `Promise`;若对应值是普通值,则会将其包装为 * 已完成的 `Promise`。 * 该方法只保证异步访问最终结果,不保证返回同一个 `Promise` 实例。 * 若名称不存在或对应工厂抛出错误,则会返回 rejected promise。 */ async getItemAsync>>( name: Name, ): Promise>> { return await Promise.resolve(this.getItem(name)) } /** * @description 重置集合中所有单例项的缓存。 * * 重置后,后续按名称访问时会重新执行各单例项的工厂函数。 */ reset(): this { this.map.forEach((item) => item.reset()) return this } } /** * @description 从多个单例项创建单例集合。 * * 集合中的单例项既可以缓存普通值,也可以缓存 `Promise`。 * * @example * ``` * // Scenario 1: sync names resolve to their corresponding cached values. * const item1 = singletonItem("count", () => 1) * const item2 = singletonItem("label", () => "ok") * const collection = singletonCollection([item1, item2]) * * // Expect: 1 * const example1 = collection.getItem("count") * // Expect: "ok" * const example2 = collection.getItem("label") * * // Scenario 2: promise-valued items keep their cached value, and async access reads the resolved result. * let asyncCalls = 0 * const item3 = singletonItem("config", async () => { * asyncCalls += 1 * return { enabled: true } * }) * const collection2 = singletonCollection([item3]) * * const example3 = collection2.getItem("config") * const example4 = collection2.getItem("config") * const example5 = await collection2.getItemAsync("config") * * // Expect: true * const example6 = example3 === example4 * // Expect: true * const example7 = example5.enabled * // Expect: 1 * const example8 = asyncCalls * * // Scenario 3: reset clears all cached singleton values in the collection. * collection2.reset() * const example9 = collection2.getItem("config") * * // Expect: false * const example10 = example4 === example9 * // Expect: 2 * const example11 = asyncCalls * ``` */ export const singletonCollection = >>( items: [...Item], ): SingletonCollection => { return new SingletonCollection(items) }