diff --git a/packages/axios/src/serviceManager.ts b/packages/axios/src/serviceManager.ts index 7b506421585c..2dacd9e38c26 100644 --- a/packages/axios/src/serviceManager.ts +++ b/packages/axios/src/serviceManager.ts @@ -34,7 +34,9 @@ export class HttpServiceFactory extends ServiceFactory { }, }; } - await this.initClients(axiosConfig); + await this.initClients(axiosConfig, { + concurrent: true, + }); } protected async createClient( diff --git a/packages/core/src/common/dataSourceManager.ts b/packages/core/src/common/dataSourceManager.ts index 8f3755752595..cf97765741af 100644 --- a/packages/core/src/common/dataSourceManager.ts +++ b/packages/core/src/common/dataSourceManager.ts @@ -9,7 +9,12 @@ import { Types } from '../util/types'; import { DEFAULT_PATTERN, IGNORE_PATTERN } from '../constants'; import { debuglog } from 'util'; import { loadModule } from '../util'; -import { ModuleLoadType, DataSourceManagerConfigOption } from '../interface'; +import { + ModuleLoadType, + DataSourceManagerConfigOption, + IDataSourceManager, + BaseDataSourceManagerConfigOption, +} from '../interface'; import { Inject } from '../decorator'; import { MidwayEnvironmentService } from '../service/environmentService'; import { MidwayPriorityManager } from './priorityManager'; @@ -18,16 +23,26 @@ const debug = debuglog('midway:debug'); export abstract class DataSourceManager< T, - ConnectionOpts extends Record = Record -> { + ConnectionOpts extends BaseDataSourceManagerConfigOption< + Record, + ENTITY_CONFIG_KEY + > = BaseDataSourceManagerConfigOption, 'entities'>, + ENTITY_CONFIG_KEY extends string = 'entities' +> implements IDataSourceManager +{ protected dataSource: Map = new Map(); - protected options: DataSourceManagerConfigOption = {}; + protected options: DataSourceManagerConfigOption< + ConnectionOpts, + ENTITY_CONFIG_KEY + > = {}; protected modelMapping = new WeakMap(); private innerDefaultDataSourceName: string; protected dataSourcePriority: Record = {}; + // for multi client with initialization + private creatingDataSources = new Map>(); @Inject() - protected appDir: string; + protected baseDir: string; @Inject() protected environmentService: MidwayEnvironmentService; @@ -36,11 +51,15 @@ export abstract class DataSourceManager< protected priorityManager: MidwayPriorityManager; protected async initDataSource( - dataSourceConfig: DataSourceManagerConfigOption, + dataSourceConfig: DataSourceManagerConfigOption< + ConnectionOpts, + ENTITY_CONFIG_KEY + >, baseDirOrOptions: | { - baseDir: string; + baseDir?: string; entitiesConfigKey?: string; + concurrent?: boolean; } | string ): Promise { @@ -55,23 +74,30 @@ export abstract class DataSourceManager< baseDirOrOptions = { baseDir: baseDirOrOptions, entitiesConfigKey: 'entities', + concurrent: false, }; } - for (const dataSourceName in dataSourceConfig.dataSource) { - const dataSourceOptions = dataSourceConfig.dataSource[dataSourceName]; - const userEntities = dataSourceOptions[ - baseDirOrOptions.entitiesConfigKey - ] as any[]; + const { + baseDir = this.baseDir, + entitiesConfigKey = 'entities', + concurrent, + } = baseDirOrOptions; + + const processDataSource = async ( + dataSourceName: string, + dataSourceOptions: any + ) => { + const userEntities = dataSourceOptions[entitiesConfigKey] as any[]; if (userEntities) { const entities = new Set(); - // loop entities and glob files to model - for (const entity of userEntities) { + + const processEntity = async (entity: any) => { if (typeof entity === 'string') { // string will be glob file - const models = await globModels( + const models = await DataSourceManager.globModels( entity, - baseDirOrOptions.baseDir, + baseDir, this.environmentService?.getModuleLoadType() ); for (const model of models) { @@ -83,21 +109,39 @@ export abstract class DataSourceManager< entities.add(entity); this.modelMapping.set(entity, dataSourceName); } + }; + + if (concurrent) { + await Promise.all(userEntities.map(processEntity)); + } else { + for (const entity of userEntities) { + await processEntity(entity); + } } - (dataSourceOptions[baseDirOrOptions.entitiesConfigKey] as any) = - Array.from(entities); + + dataSourceOptions[entitiesConfigKey] = Array.from(entities); debug( - `[core]: DataManager load ${ - dataSourceOptions[baseDirOrOptions.entitiesConfigKey].length - } models from ${dataSourceName}.` + `[core]: DataManager load ${dataSourceOptions[entitiesConfigKey].length} models from ${dataSourceName}.` ); } + // create data source const opts = { - cacheInstance: dataSourceConfig.cacheInstance, // will default true + cacheInstance: dataSourceConfig.cacheInstance, validateConnection: dataSourceConfig.validateConnection, }; - await this.createInstance(dataSourceOptions, dataSourceName, opts); + return this.createInstance(dataSourceOptions, dataSourceName, opts); + }; + + const entries = Object.entries(dataSourceConfig.dataSource); + if (concurrent) { + await Promise.all( + entries.map(([name, options]) => processDataSource(name, options)) + ); + } else { + for (const [name, options] of entries) { + await processDataSource(name, options); + } } } @@ -134,43 +178,102 @@ export abstract class DataSourceManager< return inst ? this.checkConnected(inst) : false; } + public async createInstance(config: ConnectionOpts): Promise; + public async createInstance( + config: ConnectionOpts, + clientName: string, + options?: { + /** + * @deprecated + */ + validateConnection?: boolean; + /** + * @deprecated + */ + cacheInstance?: boolean | undefined; + } + ): Promise; public async createInstance( - config: any, - clientName: any, + config: ConnectionOpts, + clientName?: any, options?: { validateConnection?: boolean; cacheInstance?: boolean | undefined; } ): Promise { - const cache = - options && typeof options.cacheInstance === 'boolean' - ? options.cacheInstance - : true; - const validateConnection = (options && options.validateConnection) || false; + if (clientName && typeof clientName !== 'string') { + options = clientName; + clientName = undefined; + } - // options.clients[id] will be merged with options.default - const configNow = extend(true, {}, this.options['default'], config); - const client = await this.createDataSource(configNow, clientName); - if (cache && clientName && client) { - this.dataSource.set(clientName, client); + if (clientName && options && options.cacheInstance === false) { + // 后面就用传不传 clientName 来判断是否缓存 + clientName = undefined; } - if (validateConnection) { - if (!client) { - throw new MidwayCommonError( - `[DataSourceManager] ${clientName} initialization failed.` - ); + if (clientName) { + if (this.dataSource.has(clientName)) { + return this.dataSource.get(clientName); + } + + if (this.creatingDataSources.has(clientName)) { + return this.creatingDataSources.get(clientName); } + } + + const validateConnection = + config.validateConnection ?? + (options && options.validateConnection) ?? + false; - const connected = await this.checkConnected(client); - if (!connected) { - throw new MidwayCommonError( - `[DataSourceManager] ${clientName} is not connected.` + // options.clients[id] will be merged with options.default + const configNow = extend(true, {}, this.options['default'], config); + + const clientCreatingPromise = this.createDataSource(configNow, clientName); + + if (clientCreatingPromise && Types.isPromise(clientCreatingPromise)) { + if (clientName) { + this.creatingDataSources.set( + clientName, + clientCreatingPromise as Promise ); } + + return (clientCreatingPromise as Promise) + .then(async client => { + if (clientName) { + this.dataSource.set(clientName, client); + } + + if (validateConnection) { + if (!client) { + throw new MidwayCommonError( + `[DataSourceManager] ${clientName} initialization failed.` + ); + } + + const connected = await this.checkConnected(client); + if (!connected) { + throw new MidwayCommonError( + `[DataSourceManager] ${clientName} is not connected.` + ); + } + } + + return client; + }) + .finally(() => { + if (clientName) { + this.creatingDataSources.delete(clientName); + } + }); } - return client; + // 处理同步返回的情况 + if (clientName) { + this.dataSource.set(clientName, clientCreatingPromise as T); + } + return clientCreatingPromise; } /** @@ -232,50 +335,53 @@ export abstract class DataSourceManager< public isLowPriority(name: string) { return this.priorityManager.isLowPriority(this.dataSourcePriority[name]); } -} -export function formatGlobString(globString: string): string[] { - let pattern; + static formatGlobString(globString: string): string[] { + let pattern; - if (!/^\*/.test(globString)) { - globString = '/' + globString; - } - const parsePattern = parse(globString); - if (parsePattern.base && (/\*/.test(parsePattern.base) || parsePattern.ext)) { - pattern = [globString]; - } else { - pattern = [...DEFAULT_PATTERN.map(p => join(globString, p))]; + if (!/^\*/.test(globString)) { + globString = '/' + globString; + } + const parsePattern = parse(globString); + if ( + parsePattern.base && + (/\*/.test(parsePattern.base) || parsePattern.ext) + ) { + pattern = [globString]; + } else { + pattern = [...DEFAULT_PATTERN.map(p => join(globString, p))]; + } + return pattern; } - return pattern; -} -export async function globModels( - globString: string, - appDir: string, - loadMode?: ModuleLoadType -) { - const pattern = formatGlobString(globString); - - const models = []; - // string will be glob file - const files = run(pattern, { - cwd: appDir, - ignore: IGNORE_PATTERN, - }); - for (const file of files) { - const exports = await loadModule(file, { - loadMode, + static async globModels( + globString: string, + baseDir: string, + loadMode?: ModuleLoadType + ) { + const pattern = this.formatGlobString(globString); + + const models = []; + // string will be glob file + const files = run(pattern, { + cwd: baseDir, + ignore: IGNORE_PATTERN, }); - if (Types.isClass(exports)) { - models.push(exports); - } else { - for (const m in exports) { - const module = exports[m]; - if (Types.isClass(module)) { - models.push(module); + for (const file of files) { + const exports = await loadModule(file, { + loadMode, + }); + if (Types.isClass(exports)) { + models.push(exports); + } else { + for (const m in exports) { + const module = exports[m]; + if (Types.isClass(module)) { + models.push(module); + } } } } + return models; } - return models; } diff --git a/packages/core/src/common/serviceFactory.ts b/packages/core/src/common/serviceFactory.ts index 60c92e8467d8..febf515f1210 100644 --- a/packages/core/src/common/serviceFactory.ts +++ b/packages/core/src/common/serviceFactory.ts @@ -2,6 +2,7 @@ import { extend } from '../util/extend'; import { IServiceFactory } from '../interface'; import { MidwayPriorityManager } from './priorityManager'; import { Inject } from '../decorator'; +import { Types } from '../util/types'; /** * 多客户端工厂实现 @@ -15,7 +16,15 @@ export abstract class ServiceFactory implements IServiceFactory { @Inject() protected priorityManager: MidwayPriorityManager; - protected async initClients(options: any = {}): Promise { + // for multi client with initialization + private creatingClients = new Map>(); + + protected async initClients( + options: any = {}, + initOptions: { + concurrent?: boolean; + } = {} + ): Promise { this.options = options; // merge options.client to options.clients['default'] @@ -25,10 +34,18 @@ export abstract class ServiceFactory implements IServiceFactory { extend(true, options.clients['default'], options.client); } - // multi client if (options.clients) { - for (const id of Object.keys(options.clients)) { - await this.createInstance(options.clients[id], id); + const entries = Object.entries(options.clients); + if (initOptions.concurrent) { + // multi client with concurrent initialization + await Promise.all( + entries.map(([id, config]) => this.createInstance(config, id)) + ); + } else { + // multi client with serial initialization + for (const [id, config] of entries) { + await this.createInstance(config, id); + } } } @@ -44,16 +61,48 @@ export abstract class ServiceFactory implements IServiceFactory { return this.clients.has(id); } - public async createInstance(config, clientName?): Promise { + public async createInstance(config: any, clientName?: string): Promise { + if (clientName) { + if (this.has(clientName)) { + return this.get(clientName); + } + + if (this.creatingClients.has(clientName)) { + return this.creatingClients.get(clientName); + } + } + // options.default will be merge in to options.clients[id] config = extend(true, {}, this.options['default'], config); - const client = await this.createClient(config, clientName); - if (client) { + + const clientCreatingPromise = this.createClient(config, clientName); + + if (clientCreatingPromise && Types.isPromise(clientCreatingPromise)) { if (clientName) { - this.clients.set(clientName, client); + this.creatingClients.set( + clientName, + clientCreatingPromise as Promise + ); } - return client; + return (clientCreatingPromise as Promise) + .then(client => { + if (clientName) { + this.clients.set(clientName, client as T); + } + return client; + }) + .finally(() => { + if (clientName) { + this.creatingClients.delete(clientName); + } + }); + } + + // 处理同步返回的情况 + if (clientName) { + this.clients.set(clientName, clientCreatingPromise as T); } + return clientCreatingPromise; } public abstract getName(): string; @@ -70,6 +119,7 @@ export abstract class ServiceFactory implements IServiceFactory { for (const [name, value] of this.clients.entries()) { await this.destroyClient(value, name); } + this.clients.clear(); } public getDefaultClientName(): string { diff --git a/packages/core/src/functional/hooks.ts b/packages/core/src/functional/hooks.ts index cbf86ae091bd..978dc09ee13f 100644 --- a/packages/core/src/functional/hooks.ts +++ b/packages/core/src/functional/hooks.ts @@ -9,7 +9,9 @@ import { ILogger, IMidwayApplication, IMidwayContainer, + IServiceFactory, MidwayConfig, + IDataSourceManager, } from '../interface'; import { MidwayConfigService } from '../service/configService'; @@ -77,3 +79,19 @@ export function useApp(appName: string): IMidwayApplication { export function useMainApp(): IMidwayApplication { return getCurrentMainApp(); } + +export async function useInjectClient( + serviceFactoryClz: new (...args) => IServiceFactory, + clientName?: string +): Promise { + const factoryService = await useInject(serviceFactoryClz); + return factoryService.get(clientName); +} + +export async function useInjectDataSource( + dataSourceManagerClz: new (...args) => IDataSourceManager, + dataSourceName: string +): Promise { + const dataSourceManager = await useInject(dataSourceManagerClz); + return dataSourceManager.getDataSource(dataSourceName); +} diff --git a/packages/core/src/interface.ts b/packages/core/src/interface.ts index d302c96a793d..386f8a2c4fb0 100644 --- a/packages/core/src/interface.ts +++ b/packages/core/src/interface.ts @@ -461,24 +461,26 @@ export type ServiceFactoryConfigOption = { export type CreateDataSourceInstanceOptions = { /** - * @default false + * @deprecated */ validateConnection?: boolean; /** - * @default true + * @deprecated */ cacheInstance?: boolean | undefined; } -export type DataSourceManagerConfigOption = { - default?: OPTIONS; +export type BaseDataSourceManagerConfigOption, ENTITY_CONFIG_KEY extends string = 'entities'> = OPTIONS & { + validateConnection?: boolean; +} & { + [key in ENTITY_CONFIG_KEY]?: any[]; +}; + +export interface DataSourceManagerConfigOption, ENTITY_CONFIG_KEY extends string = 'entities'> extends CreateDataSourceInstanceOptions { + default?: BaseDataSourceManagerConfigOption; defaultDataSourceName?: string; - dataSource?: { - [key: string]: PowerPartial<{ - [keyName in ENTITY_CONFIG_KEY]: any[]; - }> & OPTIONS; - }; -} & CreateDataSourceInstanceOptions; + dataSource?: BaseDataSourceManagerConfigOption; +} type ConfigType = T extends (...args: any[]) => any ? Writable>> @@ -1139,6 +1141,21 @@ export interface IServiceFactory { isLowPriority(clientName: string) : boolean; } +export interface IDataSourceManager { + createInstance(config: DataSourceConfig): Promise; + getDataSource(dataSourceName: string): DataSource; + getDataSourceNames(): string[]; + getAllDataSources(): Map; + hasDataSource(dataSourceName: string): boolean; + isConnected(dataSourceName: string): Promise; + getDefaultDataSourceName(): string; + stop(): Promise; + getDataSourcePriority(dataSourceName: string): string; + isHighPriority(dataSourceName: string): boolean; + isMediumPriority(dataSourceName: string): boolean; + isLowPriority(dataSourceName: string): boolean; +} + export interface ISimulation { setup?(): Promise; tearDown?(): Promise; diff --git a/packages/core/test/common/__snapshots__/dataSourceManager.test.ts.snap b/packages/core/test/common/__snapshots__/dataSourceManager.test.ts.snap index 0e45e533dffe..afdb6af63fd6 100644 --- a/packages/core/test/common/__snapshots__/dataSourceManager.test.ts.snap +++ b/packages/core/test/common/__snapshots__/dataSourceManager.test.ts.snap @@ -2,6 +2,7 @@ exports[`test/common/dataSourceManager.test.ts should test base data source 1`] = ` { + "createdAt": undefined, "dialect": "mysql", "entities": [ [Function], @@ -20,6 +21,7 @@ exports[`test/common/dataSourceManager.test.ts should test base data source 1`] exports[`test/common/dataSourceManager.test.ts should test base data source 2`] = ` { + "createdAt": undefined, "dataSourceGroup": "bb", "dialect": "mysql", "entities": [ @@ -38,6 +40,7 @@ exports[`test/common/dataSourceManager.test.ts should test base data source 2`] exports[`test/common/dataSourceManager.test.ts should test with glob model 1`] = ` { + "createdAt": undefined, "dialect": "mysql", "entities": [ [Function], diff --git a/packages/core/test/common/dataSourceManager.test.ts b/packages/core/test/common/dataSourceManager.test.ts index 2502fbb239cd..c27e0e4732e2 100644 --- a/packages/core/test/common/dataSourceManager.test.ts +++ b/packages/core/test/common/dataSourceManager.test.ts @@ -1,5 +1,4 @@ -import { DataSourceManager } from '../../src'; -import { globModels, formatGlobString } from '../../src/common/dataSourceManager'; +import { DataSourceManager, sleep } from '../../src'; import { join } from 'path'; import * as assert from 'assert'; @@ -9,13 +8,21 @@ describe('test/common/dataSourceManager.test.ts', () => { getName() { return 'test'; } - async init(options) { - return super.initDataSource(options, __dirname); + async init(options, initOptions?) { + return super.initDataSource(options, { + baseDir: __dirname, + ...initOptions + }); } protected createDataSource(config, dataSourceName: string): any { - config.entitiesLength = config.entities.length; - return config; + return new Promise(resolve => { + setTimeout(() => { + config.entitiesLength = config.entities?.length || 0; + config.createdAt = Date.now(); + resolve(config); + }, 100); + }); } protected async checkConnected(dataSource: any) { @@ -64,9 +71,17 @@ describe('test/common/dataSourceManager.test.ts', () => { }) expect(instance.getDataSourceNames()).toEqual(['default', 'test']); expect(instance.hasDataSource('default')).toBeTruthy(); - expect(instance.getDataSource('default')).toMatchSnapshot(); + const defaultDataSource = instance.getDataSource('default'); + expect({ + ...defaultDataSource, + createdAt: undefined // 忽略 createdAt 字段 + }).toMatchSnapshot(); expect(instance.getDataSource('default').entitiesLength).toEqual(2); - expect(instance.getDataSource('test')).toMatchSnapshot(); + const testDataSource = instance.getDataSource('test'); + expect({ + ...testDataSource, + createdAt: undefined // 忽略 createdAt 字段 + }).toMatchSnapshot(); expect(instance.getDataSource('test').entitiesLength).toEqual(1); expect(instance.getDataSource('fff')).not.toBeDefined(); @@ -80,10 +95,10 @@ describe('test/common/dataSourceManager.test.ts', () => { }); it('should test glob model', async () => { - let result = await globModels('dd', __dirname); + let result = await DataSourceManager.globModels('dd', __dirname); expect(result).toEqual([]); - result = await globModels('abc', __dirname); + result = await DataSourceManager.globModels('abc', __dirname); expect(result.length).toEqual(6); }); @@ -109,7 +124,11 @@ describe('test/common/dataSourceManager.test.ts', () => { }, }) expect(instance.getDataSourceNames()).toEqual(['test']); - expect(instance.getDataSource('test')).toMatchSnapshot(); + const dataSource = instance.getDataSource('test'); + expect({ + ...dataSource, + createdAt: undefined // 忽略 createdAt 字段 + }).toMatchSnapshot(); }); it('should createInstance() without cacheInstance (default true)', async () => { @@ -214,42 +233,256 @@ describe('test/common/dataSourceManager.test.ts', () => { }); it('should test glob model with pattern string', async () => { - let result = await globModels('**/bcd/**', join(__dirname, 'glob_dir_pattern')); + let result = await DataSourceManager.globModels('**/bcd/**', join(__dirname, 'glob_dir_pattern')); expect(result.length).toEqual(1); - result = await globModels('abc/*.ts', __dirname); + result = await DataSourceManager.globModels('abc/*.ts', __dirname); expect(result.length).toEqual(4); - result = await globModels('/abc', __dirname); + result = await DataSourceManager.globModels('/abc', __dirname); expect(result.length).toEqual(6); - result = await globModels('abc/a.ts', __dirname); + result = await DataSourceManager.globModels('abc/a.ts', __dirname); expect(result.length).toEqual(2); - result = await globModels('**/a.ts', __dirname); + result = await DataSourceManager.globModels('**/a.ts', __dirname); expect(result.length).toEqual(11); - result = await globModels('abc/*.ts', join(__dirname, 'glob_dir_pattern')); + result = await DataSourceManager.globModels('abc/*.ts', join(__dirname, 'glob_dir_pattern')); expect(result.length).toEqual(3); - result = await globModels('abc/**/*.ts', join(__dirname, 'glob_dir_pattern')); + result = await DataSourceManager.globModels('abc/**/*.ts', join(__dirname, 'glob_dir_pattern')); expect(result.length).toEqual(4); - result = await globModels('abc/*.entity.ts', join(__dirname, 'glob_dir_pattern')); + result = await DataSourceManager.globModels('abc/*.entity.ts', join(__dirname, 'glob_dir_pattern')); expect(result.length).toEqual(0); - result = await globModels('**/*.entity.ts', join(__dirname, 'glob_dir_pattern')); + result = await DataSourceManager.globModels('**/*.entity.ts', join(__dirname, 'glob_dir_pattern')); expect(result.length).toEqual(1); - result = await globModels('**/*.{j,t}s', join(__dirname, 'glob_dir_pattern')); + result = await DataSourceManager.globModels('**/*.{j,t}s', join(__dirname, 'glob_dir_pattern')); expect(result.length).toEqual(6); }); + describe('test concurrent initialization', () => { + class EntityA {} + class EntityB {} + + it('should initialize data sources serially by default', async () => { + const instance = new CustomDataSourceFactory(); + const startTime = Date.now(); + + await instance.init({ + dataSource: { + ds1: { + entities: [EntityA] + }, + ds2: { + entities: [EntityB] + }, + ds3: { + entities: [EntityA, EntityB] + } + } + }); + + const dataSources = Array.from(instance.getAllDataSources().values()); + const creationTimes = dataSources.map(ds => ds.createdAt); + + // 验证数据源是按顺序创建的 + for (let i = 1; i < creationTimes.length; i++) { + expect(creationTimes[i] - creationTimes[i-1]).toBeGreaterThanOrEqual(90); + } + + // 总时间应该接近 300ms (3个数据源 * 100ms) + expect(Date.now() - startTime).toBeGreaterThanOrEqual(290); + }); + + it('should initialize data sources concurrently when concurrent option is true', async () => { + const instance = new CustomDataSourceFactory(); + const startTime = Date.now(); + + await instance.init({ + dataSource: { + ds1: { + entities: [EntityA] + }, + ds2: { + entities: [EntityB] + }, + ds3: { + entities: [EntityA, EntityB] + } + } + }, { concurrent: true }); + + const dataSources = Array.from(instance.getAllDataSources().values()); + const creationTimes = dataSources.map(ds => ds.createdAt); + + // 验证所有数据源创建时间应该接近 + for (let i = 1; i < creationTimes.length; i++) { + expect(creationTimes[i] - creationTimes[i-1]).toBeLessThan(50); + } + + // 总时间应该接近 100ms + expect(Date.now() - startTime).toBeLessThan(200); + }); + + it('should handle errors in concurrent initialization', async () => { + class ErrorDataSourceFactory extends CustomDataSourceFactory { + protected createDataSource(config: any, dataSourceName: string): any { + if (dataSourceName === 'ds2') { + throw new Error('Test error'); + } + return super.createDataSource(config, dataSourceName); + } + } + + const instance = new ErrorDataSourceFactory(); + + await expect(instance.init({ + dataSource: { + ds1: { + entities: [EntityA] + }, + ds2: { + entities: [EntityB] + }, + ds3: { + entities: [EntityA, EntityB] + } + } + }, { concurrent: true })).rejects.toThrow('Test error'); + + // 验证在出错时没有数据源被创建 + expect(instance.getAllDataSources().size).toBe(0); + }); + + it('should handle entity loading concurrently', async () => { + const instance = new CustomDataSourceFactory(); + const startTime = Date.now(); + + await instance.init({ + dataSource: { + ds1: { + entities: ['/abc', '/abc', '/abc'] // 使用多个需要异步加载的实体 + } + } + }, { concurrent: true }); + + // 由于实体加载也是并发的,总时间应该远小于串行加载的时间 + expect(Date.now() - startTime).toBeLessThan(300); + + const ds = instance.getDataSource('ds1'); + expect(ds.entitiesLength).toBeGreaterThan(0); + }); + + it('should create data source only once when multiple concurrent calls', async () => { + const instance = new CustomDataSourceFactory(); + let createCount = 0; + + // 重写 createDataSource 以跟踪创建次数 + instance['createDataSource'] = async (config: any, dataSourceName: string) => { + createCount++; + await sleep(100); + return { + entitiesLength: config.entities?.length || 0, + createdAt: Date.now() + }; + }; + + // 并发调用 createInstance + const results = await Promise.all([ + instance.createInstance({}, 'same'), + instance.createInstance({}, 'same'), + instance.createInstance({}, 'same') + ]); + + // 验证只创建了一次 + expect(createCount).toBe(1); + // 验证所有结果都是同一个实例 + expect(results[0]).toBe(results[1]); + expect(results[1]).toBe(results[2]); + }); + + it('should handle errors in concurrent creation', async () => { + const instance = new CustomDataSourceFactory(); + let createCount = 0; + + // 重写 createDataSource 使其抛出错误 + instance['createDataSource'] = async () => { + createCount++; + await sleep(100); + throw new Error('Creation failed'); + }; + + // 并发调用 createInstance + const promises = Array(3).fill(0).map(() => + instance.createInstance({}, 'error-ds') + .catch(err => err) + ); + + const results = await Promise.all(promises); + + // 验证只尝试创建了一次 + expect(createCount).toBe(1); + // 验证所有调用都收到了相同的错误 + results.forEach(result => { + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('Creation failed'); + }); + // 验证 creatingDataSources 已被清理 + expect(instance['creatingDataSources'].size).toBe(0); + }); + + it('should handle sync and async creations correctly', async () => { + const instance = new CustomDataSourceFactory(); + const createCounts = new Map(); + + // 修改 createDataSource 以支持同步和异步创建 + instance['createDataSource'] = (config: any, dataSourceName: string) => { + const key = dataSourceName || 'default'; + const currentCount = createCounts.get(key) || 0; + createCounts.set(key, currentCount + 1); + + if (config.sync) { + // 即使是同步结果,也包装成 Promise + return Promise.resolve({ type: 'sync', id: currentCount + 1 }); + } else { + return (async () => { + await sleep(100); + return { type: 'async', id: currentCount + 1 }; + })(); + } + }; + + // 混合同步和异步调用 + const results = await Promise.all([ + instance.createInstance({ sync: true }, 'sync1'), + instance.createInstance({ sync: true }, 'sync1'), + instance.createInstance({}, 'async1'), + instance.createInstance({}, 'async1') + ]); + + // 验证每个类型只创建了一次 + expect(createCounts.get('sync1')).toBe(1); + expect(createCounts.get('async1')).toBe(1); + + // 验证实例复用 + expect(results[0]).toBe(results[1]); // 同步实例相同 + expect(results[2]).toBe(results[3]); // 异步实例相同 + + // 验证类型正确 + expect(results[0].type).toBe('sync'); + expect(results[2].type).toBe('async'); + }); + }); + }); describe('test global pattern', () => { it('should test parse global string', function () { - expect(formatGlobString('/entity')).toEqual([ + expect(DataSourceManager.formatGlobString('/entity')).toEqual([ '/entity/**/**.ts', '/entity/**/**.js', '/entity/**/**.mts', @@ -258,7 +491,7 @@ describe('test global pattern', () => { '/entity/**/**.cjs' ]); - expect(formatGlobString('./entity')).toEqual([ + expect(DataSourceManager.formatGlobString('./entity')).toEqual([ '/entity/**/**.ts', '/entity/**/**.js', '/entity/**/**.mts', @@ -267,7 +500,7 @@ describe('test global pattern', () => { '/entity/**/**.cjs' ]); - expect(formatGlobString('**/entity')).toEqual([ + expect(DataSourceManager.formatGlobString('**/entity')).toEqual([ '**/entity/**/**.ts', '**/entity/**/**.js', '**/entity/**/**.mts', @@ -276,7 +509,7 @@ describe('test global pattern', () => { '**/entity/**/**.cjs' ]); - expect(formatGlobString('entity')).toEqual([ + expect(DataSourceManager.formatGlobString('entity')).toEqual([ '/entity/**/**.ts', '/entity/**/**.js', '/entity/**/**.mts', @@ -285,19 +518,19 @@ describe('test global pattern', () => { '/entity/**/**.cjs' ]); - expect(formatGlobString('**/abc/**')).toEqual([ + expect(DataSourceManager.formatGlobString('**/abc/**')).toEqual([ '**/abc/**', ]); - expect(formatGlobString('**/entity/**.entity.ts')).toEqual([ + expect(DataSourceManager.formatGlobString('**/entity/**.entity.ts')).toEqual([ '**/entity/**.entity.ts', ]); - expect(formatGlobString('entity/abc.ts')).toEqual([ + expect(DataSourceManager.formatGlobString('entity/abc.ts')).toEqual([ '/entity/abc.ts', ]); - expect(formatGlobString('**/**/entity/*.entity.{j,t}s')).toEqual([ + expect(DataSourceManager.formatGlobString('**/**/entity/*.entity.{j,t}s')).toEqual([ '**/**/entity/*.entity.{j,t}s' ]); }); diff --git a/packages/core/test/common/serviceFactory.test.ts b/packages/core/test/common/serviceFactory.test.ts index 1dc10b181700..faec11e73010 100644 --- a/packages/core/test/common/serviceFactory.test.ts +++ b/packages/core/test/common/serviceFactory.test.ts @@ -1,25 +1,30 @@ -import { ServiceFactory, DEFAULT_PRIORITY, MidwayPriorityManager } from '../../src'; +import { ServiceFactory, DEFAULT_PRIORITY, MidwayPriorityManager, sleep } from '../../src'; describe('test/common/serviceFactory.test.ts', () => { class TestServiceFactory extends ServiceFactory { protected createClient(config: any): any { - const client = { - aaa: 123, - isClose: false, - async close() { - client.isClose = true; - } - }; - return client; + return new Promise(resolve => { + setTimeout(() => { + const client = { + aaa: 123, + isClose: false, + async close() { + client.isClose = true; + }, + createdAt: Date.now() + }; + resolve(client); + }, 100); + }); } getName() { return 'test'; } - async initClients(options) { - return super.initClients(options); + async initClients(options, initOptions?) { + return super.initClients(options, initOptions); } protected async destroyClient(client: any): Promise { @@ -54,9 +59,10 @@ describe('test/common/serviceFactory.test.ts', () => { }); expect(instance.get('bbb')).toBeDefined(); expect(instance.get('ccc')).toBeDefined(); - expect(instance.get('bbb').isClose).toBeFalsy(); + let ins = instance.get('bbb'); + expect(ins.isClose).toBeFalsy(); await instance.stop(); - expect(instance.get('bbb').isClose).toBeTruthy(); + expect(ins.isClose).toBeTruthy(); }); it('should test default client', async () => { @@ -137,4 +143,296 @@ describe('test/common/serviceFactory.test.ts', () => { }); }); + describe('test concurrent initialization', () => { + it('should initialize clients serially by default', async () => { + const instance = new TestServiceFactory(); + const startTime = Date.now(); + + await instance.initClients({ + clients: { + client1: {}, + client2: {}, + client3: {} + } + }); + + const clients = instance.getClients(); + const creationTimes = Array.from(clients.values()).map(client => client.createdAt); + + // 验证客户端是按顺序创建的 + for (let i = 1; i < creationTimes.length; i++) { + expect(creationTimes[i] - creationTimes[i-1]).toBeGreaterThanOrEqual(90); + } + + // 总时间应该接近 300ms (3个客户端 * 100ms) + expect(Date.now() - startTime).toBeGreaterThanOrEqual(290); + }); + + it('should initialize clients concurrently when concurrent option is true', async () => { + const instance = new TestServiceFactory(); + const startTime = Date.now(); + + await instance.initClients({ + clients: { + client1: {}, + client2: {}, + client3: {} + } + }, { concurrent: true }); + + const clients = instance.getClients(); + const creationTimes = Array.from(clients.values()).map(client => client.createdAt); + + // 验证所有客户端创建时间应该接近 + for (let i = 1; i < creationTimes.length; i++) { + expect(creationTimes[i] - creationTimes[i-1]).toBeLessThan(50); + } + + // 总时间应该接近 100ms + expect(Date.now() - startTime).toBeLessThan(200); + }); + + it('should handle errors in concurrent initialization', async () => { + class ErrorTestServiceFactory extends TestServiceFactory { + protected createClient(config: any, clientName?: string): any { + if (clientName === 'client2') { + throw new Error('Test error'); + } + return super.createClient(config); + } + } + + const instance = new ErrorTestServiceFactory(); + + await expect(instance.initClients({ + clients: { + client1: {}, + client2: {}, + client3: {} + } + }, { concurrent: true })).rejects.toThrow('Test error'); + + // 验证在出错时没有客户端被创建 + expect(instance.getClients().size).toBe(0); + }); + }); + + describe('test concurrent createInstance', () => { + class ConcurrentTestServiceFactory extends ServiceFactory { + protected async createClient(config: any, clientName?: string): Promise { + return new Promise(resolve => { + setTimeout(() => { + resolve({ + config, + clientName, + createdAt: Date.now() + }); + }, 100); + }); + } + + getName() { + return 'concurrent-test'; + } + + async initClients(options, initOptions?) { + return super.initClients(options, initOptions); + } + + protected async destroyClient(client: any): Promise { + // noop + } + } + + it('should handle mixed concurrent creations with different clientNames', async () => { + const instance = new ConcurrentTestServiceFactory(); + const creationTimes = new Map(); + + instance['createClient'] = async (config: any, clientName?: string) => { + const key = clientName || 'default'; + const currentCount = creationTimes.get(key) || 0; + creationTimes.set(key, currentCount + 1); + await sleep(100); + return { clientName: key, id: currentCount + 1 }; + }; + + // 并发调用不同的 clientName + const results = await Promise.all([ + instance.createInstance({}, 'client1'), + instance.createInstance({}, 'client2'), + instance.createInstance({}, 'client1'), // 重复的 client1 + instance.createInstance({}, 'client2'), // 重复的 client2 + instance.createInstance({}, 'client3') + ]); + + // 验证每个 clientName 只创建了一次 + expect(creationTimes.get('client1')).toBe(1); + expect(creationTimes.get('client2')).toBe(1); + expect(creationTimes.get('client3')).toBe(1); + + // 验证相同 clientName 返回相同实例 + expect(results[0]).toBe(results[2]); // client1 + expect(results[1]).toBe(results[3]); // client2 + }); + + it('should create instance only once when multiple calls with same clientName', async () => { + const instance = new TestServiceFactory(); + let createCount = 0; + + // 重写 createClient 以跟踪创建次数 + instance['createClient'] = async (config: any) => { + createCount++; + await sleep(100); // 模拟异步创建 + return { + id: createCount, + isClose: false, + async close() { + this.isClose = true; + } + }; + }; + + // 并发调用 createInstance + const results = await Promise.all([ + instance.createInstance({}, 'same'), + instance.createInstance({}, 'same'), + instance.createInstance({}, 'same') + ]); + + // 验证只创建了一次 + expect(createCount).toBe(1); + // 验证所有结果都是同一个实例 + expect(results[0]).toBe(results[1]); + expect(results[1]).toBe(results[2]); + expect(results[0].id).toBe(1); + }); + + it('should handle errors in concurrent creation', async () => { + const instance = new TestServiceFactory(); + let createCount = 0; + + // 重写 createClient 使其抛出错误 + instance['createClient'] = async () => { + createCount++; + await sleep(100); + throw new Error('Creation failed'); + }; + + // 并发调用 createInstance + const promises = Array(3).fill(0).map(() => + instance.createInstance({}, 'error-client') + .catch(err => err) + ); + + const results = await Promise.all(promises); + + // 验证只尝试创建了一次 + expect(createCount).toBe(1); + // 验证所有调用都收到了相同的错误 + results.forEach(result => { + expect(result).toBeInstanceOf(Error); + expect(result.message).toBe('Creation failed'); + }); + // 验证 creatingClients 已被清理 + expect(instance['creatingClients'].size).toBe(0); + }); + + it('should handle sync and async creations correctly', async () => { + const instance = new ConcurrentTestServiceFactory(); + const createCounts = new Map(); + + // 修改 createClient 以确保总是返回 Promise + instance['createClient'] = async (config: any, clientName?: string) => { + const key = clientName || 'default'; + const currentCount = createCounts.get(key) || 0; + createCounts.set(key, currentCount + 1); + + if (config.sync) { + // 即使是同步结果,也包装成 Promise + return Promise.resolve({ type: 'sync', id: currentCount + 1 }); + } else { + await sleep(100); + return { type: 'async', id: currentCount + 1 }; + } + }; + + // 混合同步和异步调用 + const results = await Promise.all([ + instance.createInstance({ sync: true }, 'sync1'), + instance.createInstance({ sync: true }, 'sync1'), + instance.createInstance({}, 'async1'), + instance.createInstance({}, 'async1') + ]); + + // 验证每个类型只创建了一次 + expect(createCounts.get('sync1')).toBe(1); + expect(createCounts.get('async1')).toBe(1); + + // 验证实例复用 + expect(results[0]).toBe(results[1]); // 同步实例相同 + expect(results[2]).toBe(results[3]); // 异步实例相同 + + // 验证类型正确 + expect(results[0].type).toBe('sync'); + expect(results[2].type).toBe('async'); + }); + + it('should handle synchronous createClient correctly', async () => { + class SyncTestServiceFactory extends ServiceFactory { + // 定义为同步方法 + protected createClient(config: any, clientName?: string) { + return { + id: config.id || 1, + clientName, + type: 'sync' + }; + } + + getName() { + return 'sync-test'; + } + + protected async destroyClient(client: any): Promise { + // noop + } + } + + const instance = new SyncTestServiceFactory(); + let createCount = 0; + + // 重写 createClient 以跟踪创建次数 + instance['createClient'] = (config: any, clientName?: string) => { + createCount++; + return { + id: createCount, + clientName, + type: 'sync' + }; + }; + + // 测试多次调用 + const results = await Promise.all([ + instance.createInstance({}, 'sync-client'), + instance.createInstance({}, 'sync-client'), + instance.createInstance({}, 'sync-client') + ]); + + // 验证只创建了一次 + expect(createCount).toBe(1); + + // 验证所有结果都是同一个实例 + expect(results[0]).toBe(results[1]); + expect(results[1]).toBe(results[2]); + + // 验证实例属性 + expect(results[0].type).toBe('sync'); + expect(results[0].id).toBe(1); + expect(results[0].clientName).toBe('sync-client'); + + // 验证实例已被正确缓存 + expect(instance.has('sync-client')).toBeTruthy(); + expect(instance.get('sync-client')).toBe(results[0]); + }); + }); + }); diff --git a/packages/core/test/feature.test.ts b/packages/core/test/feature.test.ts index 7dcea544a7e5..1a05cf597d3c 100644 --- a/packages/core/test/feature.test.ts +++ b/packages/core/test/feature.test.ts @@ -81,12 +81,12 @@ describe('/test/feature.test.ts', () => { const resultFail = await healthService.getStatus(); expect(resultFail).toEqual({ - "reason": "Invoke \"configuration.onHealthCheck\" running timeout(50ms)", + "reason": "Function \"configuration.onHealthCheck\" of \"__main__\" call more than 50ms", "namespace": "__main__", "results": [ { "namespace": "__main__", - "reason": "Invoke \"configuration.onHealthCheck\" running timeout(50ms)", + "reason": "Function \"configuration.onHealthCheck\" of \"__main__\" call more than 50ms", "status": false } ], diff --git a/packages/core/test/util/timeout.test.ts b/packages/core/test/util/timeout.test.ts index 9ea768e00470..18233d2f2dde 100644 --- a/packages/core/test/util/timeout.test.ts +++ b/packages/core/test/util/timeout.test.ts @@ -343,13 +343,13 @@ describe('test create timeout handler', () => { const promise = createPromiseTimeoutInvokeChain({ promiseItems: [{ item: async (ac) => { - await sleep(100, ac); + await sleep(200, ac); return 3; }, itemName: 'p1', }], methodName: 'configuration.onReady', - itemTimeout: 200, + itemTimeout: 100, }); await expect(promise).rejects.toThrow('Function \"configuration.onReady\" of \"p1\" call more than 100ms'); diff --git a/packages/cos/src/manager.ts b/packages/cos/src/manager.ts index 1808684c8743..b1c297e82dfd 100644 --- a/packages/cos/src/manager.ts +++ b/packages/cos/src/manager.ts @@ -21,7 +21,9 @@ export class COSServiceFactory extends ServiceFactory { @Init() async init() { - await this.initClients(this.cosConfig); + await this.initClients(this.cosConfig, { + concurrent: true, + }); } @Logger('coreLogger') diff --git a/packages/etcd/src/manager.ts b/packages/etcd/src/manager.ts index 4450e5d34fdf..1f9b0d682ec8 100644 --- a/packages/etcd/src/manager.ts +++ b/packages/etcd/src/manager.ts @@ -20,7 +20,9 @@ export class ETCDServiceFactory extends ServiceFactory { @Init() async init() { - await this.initClients(this.etcdConfig); + await this.initClients(this.etcdConfig, { + concurrent: true, + }); } @Logger('coreLogger') diff --git a/packages/info/test/__snapshots__/index.test.ts.snap b/packages/info/test/__snapshots__/index.test.ts.snap index 9c09aacaf2c9..c80e2c583473 100644 --- a/packages/info/test/__snapshots__/index.test.ts.snap +++ b/packages/info/test/__snapshots__/index.test.ts.snap @@ -4,7 +4,7 @@ exports[`test/index.test.ts should test get config and filter secret 1`] = ` { "info": { "asyncContextManager": "{"enable":true}", - "core": "{"healthCheckTimeout":1000}", + "core": "{"healthCheckTimeout":1000,"configLoadTimeout":10000,"readyTimeout":30000,"serverReadyTimeout":30000}", "cos": "{"default":{"SecretId":"df******af","SecretKey":"te******vc","SecurityToken":"fj*********af"}}", "debug": "{"recordConfigMergeOrder":true}", "info": "{"title":"Midway Info","infoPath":"/_info","hiddenKey":["k***","****","*****n","********","p****"],"ignoreKey":[]}", diff --git a/packages/kafka/src/service.ts b/packages/kafka/src/service.ts index 16c4abc2d92f..2cac76ba5db4 100644 --- a/packages/kafka/src/service.ts +++ b/packages/kafka/src/service.ts @@ -30,7 +30,9 @@ export class KafkaProducerFactory extends ServiceFactory { @Init() async init() { - await this.initClients(this.pubConfig); + await this.initClients(this.pubConfig, { + concurrent: true, + }); } protected async createClient( @@ -84,7 +86,9 @@ export class KafkaAdminFactory extends ServiceFactory { @Init() async init() { - await this.initClients(this.adminConfig); + await this.initClients(this.adminConfig, { + concurrent: true, + }); } protected async createClient( diff --git a/packages/leoric/src/dataSourceManager.ts b/packages/leoric/src/dataSourceManager.ts index 5d969212078b..c7347bff5149 100644 --- a/packages/leoric/src/dataSourceManager.ts +++ b/packages/leoric/src/dataSourceManager.ts @@ -3,7 +3,6 @@ import { DataSourceManager, ILogger, Init, - Inject, Logger, Provide, Scope, @@ -24,9 +23,6 @@ export class LeoricDataSourceManager extends DataSourceManager< @Logger('coreLogger') coreLogger: ILogger; - @Inject() - baseDir: string; - @Init() async init() { if (Object.keys(this.leoricConfig.dataSource).length > 1) { @@ -35,8 +31,8 @@ export class LeoricDataSourceManager extends DataSourceManager< } } await this.initDataSource(this.leoricConfig, { - baseDir: this.baseDir, entitiesConfigKey: 'models', + concurrent: true, }); } diff --git a/packages/mikro/src/dataSourceManager.ts b/packages/mikro/src/dataSourceManager.ts index f414d167a9c6..8e03b9b7d53a 100644 --- a/packages/mikro/src/dataSourceManager.ts +++ b/packages/mikro/src/dataSourceManager.ts @@ -18,15 +18,14 @@ export class MikroDataSourceManager extends DataSourceManager< @Config('mikro') mikroConfig; - @Inject() - baseDir: string; - @Inject() loggerService: MidwayLoggerService; @Init() async init() { - await this.initDataSource(this.mikroConfig, this.baseDir); + await this.initDataSource(this.mikroConfig, { + concurrent: true, + }); } getName(): string { diff --git a/packages/mongoose/src/manager.ts b/packages/mongoose/src/manager.ts index 763e587b34a0..a6f60969ab14 100644 --- a/packages/mongoose/src/manager.ts +++ b/packages/mongoose/src/manager.ts @@ -22,9 +22,6 @@ export class MongooseDataSourceManager extends DataSourceManager { @Init() async init() { - await this.initClients(this.pubConfig); + await this.initClients(this.pubConfig, { + concurrent: true, + }); } protected async createClient( diff --git a/packages/oss/src/manager.ts b/packages/oss/src/manager.ts index 33859dee26d0..bf51e3eaa7e9 100644 --- a/packages/oss/src/manager.ts +++ b/packages/oss/src/manager.ts @@ -38,7 +38,9 @@ export class OSSServiceFactory< @Init() async init() { - await this.initClients(this.ossConfig); + await this.initClients(this.ossConfig, { + concurrent: true, + }); } async createClient( diff --git a/packages/processAgent/package.json b/packages/processAgent/package.json index 772b3695d196..acd44191b693 100644 --- a/packages/processAgent/package.json +++ b/packages/processAgent/package.json @@ -1,6 +1,7 @@ { "name": "@midwayjs/process-agent", "version": "4.0.0-alpha.1", + "private": true, "description": "", "main": "dist/index.js", "typings": "dist/index.d.ts", diff --git a/packages/redis/src/manager.ts b/packages/redis/src/manager.ts index f34ea7c9649b..f9b129bfd6a3 100644 --- a/packages/redis/src/manager.ts +++ b/packages/redis/src/manager.ts @@ -25,7 +25,9 @@ export class RedisServiceFactory extends ServiceFactory { @Init() protected async init() { - await this.initClients(this.redisConfig); + await this.initClients(this.redisConfig, { + concurrent: true, + }); } @Logger('coreLogger') diff --git a/packages/sequelize/src/configuration.ts b/packages/sequelize/src/configuration.ts index 7013c6ef7d50..2a2df6fb3a4b 100644 --- a/packages/sequelize/src/configuration.ts +++ b/packages/sequelize/src/configuration.ts @@ -15,9 +15,7 @@ import { Model } from 'sequelize-typescript'; importConfigs: [ { default: { - sequelize: { - validateConnection: true, - }, + sequelize: {}, }, }, ], diff --git a/packages/sequelize/src/dataSourceManager.ts b/packages/sequelize/src/dataSourceManager.ts index 062ea38907a6..6c6da73db461 100644 --- a/packages/sequelize/src/dataSourceManager.ts +++ b/packages/sequelize/src/dataSourceManager.ts @@ -4,7 +4,6 @@ import { Logger, Provide, Scope, - Inject, ScopeEnum, DataSourceManager, ILogger, @@ -20,12 +19,11 @@ export class SequelizeDataSourceManager extends DataSourceManager { @Logger('coreLogger') coreLogger: ILogger; - @Inject() - baseDir: string; - @Init() async init() { - await this.initDataSource(this.sequelizeConfig, this.baseDir); + await this.initDataSource(this.sequelizeConfig, { + concurrent: true, + }); } getName(): string { diff --git a/packages/sequelize/test/index.test.ts b/packages/sequelize/test/index.test.ts index 596c1bf353c3..a0332abfa0c8 100644 --- a/packages/sequelize/test/index.test.ts +++ b/packages/sequelize/test/index.test.ts @@ -73,9 +73,9 @@ describe('/test/index.test.ts', () => { dialect: 'mysql', host: '', port: 3306, + validateConnection: false, }, }, - validateConnection: false, }, }, }); @@ -101,6 +101,7 @@ describe('/test/index.test.ts', () => { dialect: 'mysql', host: '', port: 3306, + validateConnection: true, }, }, }, diff --git a/packages/tablestore/src/manager.ts b/packages/tablestore/src/manager.ts index e97b096b612c..a9924f214f10 100644 --- a/packages/tablestore/src/manager.ts +++ b/packages/tablestore/src/manager.ts @@ -20,7 +20,9 @@ export class TableStoreServiceFactory extends ServiceFactory { @Init() async init() { - await this.initClients(this.tableStoreConfig); + await this.initClients(this.tableStoreConfig, { + concurrent: true, + }); } async createClient(config): Promise { diff --git a/packages/tags/package.json b/packages/tags/package.json index f57ce04e8163..4a5edf712fb9 100644 --- a/packages/tags/package.json +++ b/packages/tags/package.json @@ -1,6 +1,7 @@ { "name": "@midwayjs/tags", "version": "4.0.0-alpha.1", + "private": true, "description": "Midway Tag System", "main": "dist/index.js", "typings": "index.d.ts", diff --git a/packages/typeorm/src/dataSourceManager.ts b/packages/typeorm/src/dataSourceManager.ts index c2721c7334b5..fc71758000a3 100644 --- a/packages/typeorm/src/dataSourceManager.ts +++ b/packages/typeorm/src/dataSourceManager.ts @@ -22,15 +22,14 @@ export class TypeORMDataSourceManager extends DataSourceManager { @ApplicationContext() applicationContext: IMidwayContainer; - @Inject() - baseDir: string; - @Inject() loggerService: MidwayLoggerService; @Init() async init() { - await this.initDataSource(this.typeormConfig, this.baseDir); + await this.initDataSource(this.typeormConfig, { + concurrent: true, + }); } getName(): string { diff --git a/site/docs/data_source.md b/site/docs/data_source.md index 249649115ad2..8e33e6531647 100644 --- a/site/docs/data_source.md +++ b/site/docs/data_source.md @@ -111,21 +111,27 @@ export class MySqlDataSourceManager extends DataSourceManager @Config('mysql') mysqlConfig; - @Inject() - baseDir: string; - @Init() async init() { - // 需要注意的是,这里第二个参数需要传入一个实体类扫描地址 - await this.initDataSource(this.mysqlConfig, this.baseDir); + await this.initDataSource(this.mysqlConfig, { + concurrent: true + }); } // ... } - ``` +从 v4.0.0 开始,`initDataSource` 方法支持第二个参数,用于传递初始化选项。 + +可选的值有: + +- `baseDir`: 实体类扫描起始地址,可选,默认是 `src` 或者 `dist` +- `entitiesConfigKey`: 实体类配置键,框架会从配置中的这个 key 获取实体类,可选,默认是 `entities` +- `concurrent`: 是否并发初始化,可选,为了向前兼容,默认是 `false`。 + + 在 `src/config/config.default` 中,我们可以提供多数据源的配置,来创建多个数据源。 比如: @@ -156,15 +162,83 @@ export const mysql = { 数据源天然就是为了多个实例而设计的,和服务工厂不同,没有单个和多个的配置区别。 +### 3、实例化数据源管理器 +为了方便用户使用,我们还需要提前将数据源管理器创建,一般来说,只需要在组件或者项目的 `onReady` 生命周期中实例化,在 `onStop` 生命周期中销毁。 -## 实体绑定 +```typescript +import { Configuration } from '@midwayjs/core'; + +@Configuration({ + imports: [ + // ... + ] +}) +export class ContainerConfiguration { + private mysqlDataSourceManager: MySqlDataSourceManager; + + async onReady(container) { + // 实例化数据源管理器 + this.mysqlDataSourceManager = await container.getAsync(MySqlDataSourceManager); + } -数据源最重要的一环是实体类,每个数据源都可以拥有自己的实体类。比如 typeorm 等 orm 框架,都是基于此来设计的。 + async onStop() { + // 销毁数据源管理器 + if (this.mysqlDataSourceManager) { + await this.mysqlDataSourceManager.stop(); + } + } +} +``` + +## 数据源配置 + +在 `src/config/config.default` 中,多个数据源配置格式和 [服务工厂](./service_factory) 类似。 + +默认的配置,我们约定为 `default` 属性。 +在创建数据源时,普通的数据源配置以及动态创建的数据源配置都会和 `default` 配置合并。 +和服务工厂略有不同,数据源天然就是为了多个实例而设计的,没有单个配置的情况。 -### 1、显式关联实体类 +完整结构如下: + +```typescript +// config.default.ts +export const mysql = { + default: { + // 默认数据源配置 + } + dataSource: { + dataSource1: { + entities: [], + validateConnection: false, + // 数据源配置 + }, + dataSource2: { + // 数据源配置 + }, + dataSource3: { + // 数据源配置 + }, + } + // 其他配置 +} +``` + +:::tip +- `entities` 是框架提供的特有的配置,用于指定实体类。 +- `validateConnection` 是框架提供的特有的配置,用于指定是否通过 `checkConnected` 方法验证连接。 +::: + + + +## 绑定实体类 + +数据源最重要的一环是实体类,每个数据源都可以拥有自己的实体类。比如 typeorm 等 orm 框架,都是基于此来设计的。 + + +### 显式关联实体类 实体类一般是和表结构相同的类。 @@ -220,7 +294,7 @@ export default { -### 2、目录扫描关联实体 +### 目录扫描关联实体 在某些情况下,我们也可以通过通配的路径来替代,比如: @@ -257,15 +331,17 @@ export default { 注意 -- 1、填写目录字符串时,以 initDataSource 方法的第二个参数作为相对路径查找,默认为 baseDir(src 或者 dist) -- 2、如果匹配后缀,entities 的路径注意包括 js 和 ts 后缀,否则编译后会找不到实体 +- 1、填写目录字符串时,以 `initDataSource` 方法的第二个参数作为相对路径查找,默认为 `baseDir`(`src` 或者 `dist`) +- 2、如果匹配后缀,`entities` 的路径注意包括 `js` 和 `ts` 后缀,否则编译后会找不到实体 - 3、字符串路径的写法不支持 [单文件构建部署](./deployment#单文件构建部署)(bundle模式) ::: +## 获取数据源 + -### 2、根据实体获取数据源 +### 根据实体获取数据源 一般我们的 API 都是在数据源对象上,比如 `connection.query`。 @@ -328,7 +404,7 @@ export const mysql = { -## 获取数据源 +### 动态 API 获取数据源 通过注入数据源管理器,我们可以通过其上面的方法来拿到数据源。 @@ -362,3 +438,136 @@ this.mysqlDataSourceManager.getDataSourceNames(); this.mysqlDataSourceManager.isConnected('dataSource1') ``` + + +## 动态创建数据源 + +除了通过配置初始化数据源外,我们还可以在运行时动态创建数据源。这在需要根据不同条件创建数据源的场景下非常有用。 + +使用 `createInstance` 方法可以动态创建数据源: + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { MySqlDataSourceManager } from './manager/mysqlDataSourceManager'; + +@Provide() +export class UserService { + @Inject() + mysqlDataSourceManager: MySqlDataSourceManager; + + async createNewDataSource() { + // 创建新的数据源 + const dataSource = await this.mysqlDataSourceManager.createInstance({ + host: 'localhost', + user: 'root', + database: 'new_db', + entities: ['entity/user.entity.ts'], + validateConnection: true + }, 'dynamicDB'); + + // 使用新创建的数据源 + // ... + } +} +``` + +`createInstance` 方法接受两个参数: +- `config`: 数据源配置 +- `dataSourceName`: 数据源名称(可选) + +:::tip +- 1、`dataSourceName` 是数据源的唯一标识,用于区分不同的数据源。 +- 2、如果不提供 `dataSourceName`,则数据源管理器不会缓存该数据源,返回后需要用户自行管理其生命周期。 +- 3、动态创建的数据源,会和 `default` 配置合并。 +::: + +## 类型定义 + +在使用数据源时,我们需要正确定义类型。Midway 提供了两个核心类型来帮助你定义数据源配置。 + +#### BaseDataSourceManagerConfigOption + +用于定义基础数据源配置: + +```typescript +import { BaseDataSourceManagerConfigOption } from '@midwayjs/core'; + +// 定义你的数据源配置 +interface MySQLOptions { + host: string; + port: number; + username: string; + password: string; + database: string; +} + +// 使用 BaseDataSourceManagerConfigOption 定义完整配置 +// 第一个泛型参数是数据源配置 +// 第二个泛型参数是实体配置的键名(默认为 'entities') +type MySQLConfig = BaseDataSourceManagerConfigOption; +``` + +#### DataSourceManagerConfigOption + +在基础配置的基础上,增加了数据源管理相关的配置: + +```typescript +import { DataSourceManagerConfigOption } from '@midwayjs/core'; + +interface MySQLOptions { + host: string; + port: number; + username: string; + password: string; + database: string; +} + +// 使用 DataSourceManagerConfigOption 定义配置 +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + mysql?: DataSourceManagerConfigOption; + } +} +``` + +#### 使用示例 + +```typescript +// src/config/config.default.ts +export default { + mysql: { + // 默认数据源配置 + default: { + host: 'localhost', + port: 3306, + username: 'root', + password: '123456', + database: 'test', + entities: ['entity/**/*.entity.ts'], + validateConnection: true + }, + // 多数据源配置 + dataSource: { + db1: { + host: 'localhost', + port: 3306, + username: 'root', + password: '123456', + database: 'db1', + entities: ['entity/db1/**/*.entity.ts'] + } + } + } +} +``` + +:::tip +- `BaseDataSourceManagerConfigOption` 主要用于定义单个数据源的配置 +- `DataSourceManagerConfigOption` 用于定义完整的数据源管理配置,包括多数据源支持 +- 如果你的 ORM 使用不同的实体配置键名,可以通过第二个泛型参数指定,如: + ```typescript + // Sequelize 使用 models 而不是 entities + type SequelizeConfig = DataSourceManagerConfigOption; + ``` +::: + diff --git a/site/docs/service_factory.md b/site/docs/service_factory.md index a175d6c21c74..cbb92acab42c 100644 --- a/site/docs/service_factory.md +++ b/site/docs/service_factory.md @@ -48,7 +48,7 @@ import { ServiceFactory, Provide, Scope, ScopeEnum } from '@midwayjs/core'; @Provide() @Scope(ScopeEnum.Singleton) export class HTTPClientServiceFactory extends ServiceFactory { - // ... + // ... } ``` 由于是抽象类,我们需要实现其中的两个方法。 @@ -83,7 +83,7 @@ export class HTTPClientServiceFactory extends ServiceFactory { ```typescript // config.default.ts export const httpClient = { - // ... + // ... } ``` 然后注入到服务工厂中,同时,我们还需要在初始化时,调用创建多个实例的方法。 @@ -112,8 +112,21 @@ export class HTTPClientServiceFactory extends ServiceFactory { } } ``` + `initClients` 方法是基类中实现的,它需要传递一个完整的用户配置,并循环调用 `createClient` 来创建对象,保存到内存中。 +从 v4.0.0 开始,`initClients` 方法支持第二个参数,用于传递初始化选项。 + +你可以通过 `concurrent` 参数来控制是否并发初始化,为了向前兼容,默认是 `false`。 + +如果确认所有实例的初始化之间没有干扰,可以设置为 `true`,以提高初始化速度。 + +```typescript +await this.initClients(this.httpClientConfig, { + concurrent: true +}); +``` + ### 3、实例化服务类 @@ -128,6 +141,7 @@ import { Configuration } from '@midwayjs/core'; ] }) export class ContainerConfiguration { + async onReady(container) { // 实例化服务类 await container.getAsync(HTTPClientServiceFactory); @@ -156,6 +170,9 @@ export class ContainerConfiguration { 默认的配置,我们约定为 `default` 属性。 + +在创建实例时,普通的实例配置以及动态创建的实例配置都会和 `default` 配置合并。 + ```typescript // config.default.ts export const httpClient = { @@ -177,7 +194,7 @@ export const httpClient = { timeout: 3000 }, client: { - baseUrl: '' + baseUrl: '' } } ``` @@ -211,11 +228,11 @@ export const httpClient = { timeout: 3000 }, clients: { - aaa: { - baseUrl: '' + aaa: { + baseUrl: '' }, bbb: { - baseUrl: '' + baseUrl: '' } } } @@ -265,7 +282,7 @@ export class UserService { async invoke() { // this.aaaInstance.xxx - // this.bbbInstance.xxx + // this.bbbInstance.xxx // ... } } @@ -339,7 +356,7 @@ export class UserService { // config.default.ts export const httpClient = { client: { - baseUrl: '' + baseUrl: '' } } ``` @@ -461,7 +478,7 @@ export class UserService { httpClientService: HTTPClientService; async invoke() { - // this.httpClientService 中指向的是 default2 + // this.httpClientService 中指向的是 default2 } } ``` @@ -472,7 +489,7 @@ export class UserService { 从 v3.14.0 开始,服务工厂的实例可以增加一个优先级属性,在不同的场景,会根据优先级做一些不同处理。 -实例的优先级有 `L1`,`L2`, `L3`三个等级,分别对应高,中,低三个层级。 +实例的优先级有 `L1`,`L2`, `L3` 三个等级,分别对应高,中,低三个层级。 定义如下: @@ -532,3 +549,69 @@ export class HTTPClientService implements HTTPClient { } ``` + +## 销毁服务类和实例 + +服务工厂提供了 `destroyClient` 方法,会在服务工厂类 `stop` 方法调用时被自动调用,用于销毁实例,如果不需要销毁实例,可以不实现。 + +```typescript +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientServiceFactory extends ServiceFactory { + // ... + + async destroyClient(client: HTTPClient, clientName: string) { + // 销毁实例 + } +} +``` + +除了实现 `destroyClient` 方法,还需要在生命周期中显式调用 `stop` 方法。 + +```typescript +import { Configuration } from '@midwayjs/core'; + +@Configuration({ + imports: [ + // ... + ] +}) +export class ContainerConfiguration { + private httpClientServiceFactory: HTTPClientServiceFactory; + + async onReady(container) { + // 实例化服务类 + this.httpClientServiceFactory = await container.getAsync(HTTPClientServiceFactory); + } + + async onStop() { + // 销毁服务类 + if (this.httpClientServiceFactory) { + await this.httpClientServiceFactory.stop(); + } + } +} +``` + +## 类型定义 + + +在使用服务工厂时,我们需要正确定义类型。Midway 提供了 `ServiceFactoryConfigOption` 核心类型来帮助你定义服务工厂配置。 + + +```typescript +import { ServiceFactoryConfigOption } from '@midwayjs/core'; + +// 定义 HTTPClient 配置 +interface HTTPClientConfig { + baseUrl: string; + timeout?: number; +} + +// 使用 ServiceFactoryConfigOption 定义配置 +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + httpClient?: ServiceFactoryConfigOption; + } +} +``` diff --git a/site/i18n/en/docusaurus-plugin-content-docs/current/data_source.md b/site/i18n/en/docusaurus-plugin-content-docs/current/data_source.md index 6214f3c82b36..227459512108 100644 --- a/site/i18n/en/docusaurus-plugin-content-docs/current/data_source.md +++ b/site/i18n/en/docusaurus-plugin-content-docs/current/data_source.md @@ -111,13 +111,11 @@ export class MySqlDataSourceManager extends DataSourceManager @Config('mysql') mysqlConfig; - @Inject() - baseDir: string; - @Init() async init() { - // It should be noted that the second parameter here needs to pass in an entity class scan address - await this.initDataSource(this.mysqlConfig, this.baseDir); + await this.initDataSource(this.mysqlConfig, { + concurrent: true + }); } // ... @@ -126,6 +124,16 @@ export class MySqlDataSourceManager extends DataSourceManager ``` + +Starting from v4.0.0, the `initDataSource` method supports a second parameter for passing initialization options. + +Optional values are: + +- `baseDir`: Entity class scanning start address, optional, default is `src` or `dist` +- `entitiesConfigKey`: Entity class configuration key, the framework will get entity classes from this key in the configuration, optional, default is `entities` +- `concurrent`: Whether to initialize concurrently, optional, for backward compatibility, the default is `false` + + In the `src/config/config.default`, we can provide the configuration of multiple data sources to create multiple data sources. For example: @@ -158,15 +166,55 @@ Data sources are naturally designed for multiple instances. Unlike service facto +## Data Source Configuration + +In `src/config/config.default`, the configuration format for multiple data sources is similar to [service factory](./service_factory). + +The default configuration is agreed to be the `default` property. + +When creating a data source, both regular data source configurations and dynamically created data source configurations will be merged with the `default` configuration. + +Unlike service factories, data sources are naturally designed for multiple instances, and there is no single configuration case. + +The complete structure is as follows: + +```typescript +// config.default.ts +export const mysql = { + default: { + // Default data source configuration + } + dataSource: { + dataSource1: { + entities: [], + validateConnection: false, + // Data source configuration + }, + dataSource2: { + // Data source configuration + }, + dataSource3: { + // Data source configuration + }, + } + // Other configurations +} +``` + +:::tip +- `entities` is a framework-specific configuration used to specify entity classes. +- `validateConnection` is a framework-specific configuration used to specify whether to validate the connection through the `checkConnected` method. +::: + -## Entity binding +## Binding Entity Classes The most important part of the data source is the entity class, each data source can have its own entity class. For example, orm frameworks such as typeorm are designed based on this. -### 1. Explicitly associate entity classes +### Explicitly Associate Entity Classes Entity classes are generally the same class as the table structure. @@ -222,7 +270,7 @@ The `entities` configuration of each data source can add its own entity class. -### 2. Directory Scan Associated Entities +### Directory Scan Associated Entities In some cases, we can also replace it with a matching path, such: @@ -259,15 +307,16 @@ export default { Attention -- 1. When filling in the directory string, use the second parameter of the initDataSource method as a relative path search, and the default is baseDir (src or dist) +- 1. When filling in the directory string, use the `initDataSource` method's second parameter as a relative path search, and the default is baseDir (src or dist) - 2. If the suffix is matched, the path of entities should include the js and ts suffixes, otherwise the entity will not be found after compilation - 3. The writing method of the string path does not support [single-file build deployment](./deployment#single-file build deployment) (bundle mode) ::: +## Getting Data Sources -### 2. Obtain the data source according to the entity +### Getting Data Sources by Entity Generally, our API is on data source objects, such as `connection.query`. @@ -329,7 +378,7 @@ export const mysql = { -## Get data source +### Dynamic API to Get Data Sources By injecting the data source manager, we can get the data source through the above methods. @@ -363,3 +412,136 @@ this.mysqlDataSourceManager.getDataSourceNames(); this.mysqlDataSourceManager.isConnected('dataSource1') ``` + +## Dynamic Data Source Creation + +In addition to initializing data sources through configuration, we can also dynamically create data sources at runtime. This is very useful in scenarios where data sources need to be created based on different conditions. + +Use the `createInstance` method to dynamically create a data source: + +```typescript +import { Provide, Inject } from '@midwayjs/core'; +import { MySqlDataSourceManager } from './manager/mysqlDataSourceManager'; + +@Provide() +export class UserService { + @Inject() + mysqlDataSourceManager: MySqlDataSourceManager; + + async createNewDataSource() { + // Create new data source + const dataSource = await this.mysqlDataSourceManager.createInstance({ + host: 'localhost', + user: 'root', + database: 'new_db', + entities: ['entity/user.entity.ts'], + validateConnection: true + }, 'dynamicDB'); + + // Use the newly created data source + // ... + } +} +``` + +The `createInstance` method accepts two parameters: +- `config`: Data source configuration +- `dataSourceName`: Data source name (optional) + +:::tip +- 1. `dataSourceName` is the unique identifier of the data source, used to distinguish different data sources. +- 2. If `dataSourceName` is not provided, the data source manager will not cache this data source, and users need to manage its lifecycle themselves after return. +- 3. Dynamically created data sources will be merged with the `default` configuration. +::: + + +## Type Definition + +When using data sources, we need to correctly define types. Midway provides two core types to help you define data source configurations. + +#### BaseDataSourceManagerConfigOption + +Used to define basic data source configuration: + +```typescript +import { BaseDataSourceManagerConfigOption } from '@midwayjs/core'; + +// Define your data source configuration +interface MySQLOptions { + host: string; + port: number; + username: string; + password: string; + database: string; +} + +// Use BaseDataSourceManagerConfigOption to define complete configuration +// The first generic parameter is the data source configuration +// The second generic parameter is the entity configuration key name (default is 'entities') +type MySQLConfig = BaseDataSourceManagerConfigOption; +``` + +#### DataSourceManagerConfigOption + +Adds data source management related configuration on top of the basic configuration: + +```typescript +import { DataSourceManagerConfigOption } from '@midwayjs/core'; + +interface MySQLOptions { + host: string; + port: number; + username: string; + password: string; + database: string; +} + +// Use DataSourceManagerConfigOption to define configuration +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + mysql?: DataSourceManagerConfigOption; + } +} +``` + +#### Usage Example + +```typescript +// src/config/config.default.ts +export default { + mysql: { + // Default data source configuration + default: { + host: 'localhost', + port: 3306, + username: 'root', + password: '123456', + database: 'test', + entities: ['entity/**/*.entity.ts'], + validateConnection: true + }, + // Multiple data source configuration + dataSource: { + db1: { + host: 'localhost', + port: 3306, + username: 'root', + password: '123456', + database: 'db1', + entities: ['entity/db1/**/*.entity.ts'] + } + } + } +} +``` + +:::tip +- `BaseDataSourceManagerConfigOption` is mainly used to define configuration for a single data source +- `DataSourceManagerConfigOption` is used to define complete data source management configuration, including multiple data source support +- If your ORM uses a different entity configuration key name, you can specify it through the second generic parameter, such as: + ```typescript + // Sequelize uses models instead of entities + type SequelizeConfig = DataSourceManagerConfigOption; + ``` +::: + diff --git a/site/i18n/en/docusaurus-plugin-content-docs/current/service_factory.md b/site/i18n/en/docusaurus-plugin-content-docs/current/service_factory.md index d8e4f7b27b5d..b45ba6b68b79 100644 --- a/site/i18n/en/docusaurus-plugin-content-docs/current/service_factory.md +++ b/site/i18n/en/docusaurus-plugin-content-docs/current/service_factory.md @@ -114,6 +114,17 @@ export class HTTPClientServiceFactory extends ServiceFactory { ``` `initClients` method is implemented in the base class. It needs to pass a complete user configuration and call the `createClient` in a loop to create the object and save it to memory. +Starting from v4.0.0, the `initClients` method supports a second parameter for passing initialization options. + +You can control whether to initialize concurrently through the `concurrent` parameter. For backward compatibility, the default is `false`. + +If you are sure that there is no interference between all instance initializations, you can set it to `true` to improve initialization speed. + +```typescript +await this.initClients(this.httpClientConfig, { + concurrent: true +}); +``` ### 3. Instantiate service class @@ -155,6 +166,9 @@ Let's explain separately, The default configuration, we agreed to `default` the attribute. + +When creating an instance, the normal instance configuration and dynamically created instance configuration will be merged with the `default` configuration. + ```typescript // config.default.ts export const httpClient = { @@ -530,3 +544,67 @@ export class HTTPClientService implements HTTPClient { } } ``` + +## Destroy service class and instances + +The service factory provides a `destroyClient` method, which will be automatically called when the service factory class `stop` method is called, used to destroy instances. If you don't need to destroy instances, you don't need to implement it. + +```typescript +@Provide() +@Scope(ScopeEnum.Singleton) +export class HTTPClientServiceFactory extends ServiceFactory { + // ... + + async destroyClient(client: HTTPClient, clientName: string) { + // Destroy instance + } +} +``` + +In addition to implementing the `destroyClient` method, you also need to explicitly call the `stop` method in the lifecycle. + +```typescript +import { Configuration } from '@midwayjs/core'; + +@Configuration({ + imports: [ + // ... + ] +}) +export class ContainerConfiguration { + private httpClientServiceFactory: HTTPClientServiceFactory; + + async onReady(container) { + // Instantiate service class + this.httpClientServiceFactory = await container.getAsync(HTTPClientServiceFactory); + } + + async onStop() { + // Destroy service class + if (this.httpClientServiceFactory) { + await this.httpClientServiceFactory.stop(); + } + } +} +``` + +## Type Definition + +When using the service factory, we need to correctly define the types. Midway provides the `ServiceFactoryConfigOption` core type to help you define service factory configuration. + +```typescript +import { ServiceFactoryConfigOption } from '@midwayjs/core'; + +// Define HTTPClient configuration +interface HTTPClientConfig { + baseUrl: string; + timeout?: number; +} + +// Use ServiceFactoryConfigOption to define configuration +declare module '@midwayjs/core/dist/interface' { + interface MidwayConfig { + httpClient?: ServiceFactoryConfigOption; + } +} +```