diff --git a/lib/core/singleton.js b/lib/core/singleton.js index 9f8d369615..3705bf55aa 100644 --- a/lib/core/singleton.js +++ b/lib/core/singleton.js @@ -1,6 +1,7 @@ 'use strict'; const assert = require('assert'); +const is = require('is-type-of'); class Singleton { constructor(options = {}) { @@ -16,25 +17,28 @@ class Singleton { this.options = options.app.config[this.name] || {}; } - init() { + async init() { const options = this.options; assert(!(options.client && options.clients), `egg:singleton ${this.name} can not set options.client and options.clients both`); // alias app[name] as client, but still support createInstance method if (options.client) { - const client = this.createInstance(options.client); + const client = await this.createInstanceAsync(options.client); this.app[this.name] = client; assert(!client.createInstance, 'singleton instance should not have createInstance method'); + assert(!client.createInstanceAsync, 'singleton instance should not have createInstanceAsync method'); client.createInstance = this.createInstance.bind(this); + client.createInstanceAsync = this.createInstanceAsync.bind(this); return; } // multi clent, use app[name].getInstance(id) if (options.clients) { - for (const id in options.clients) { - this.clients.set(id, this.createInstance(options.clients[id])); - } + await Promise.all(Object.keys(options.clients).map(id => { + return this.createInstanceAsync(options.clients[id]) + .then(client => this.clients.set(id, client)); + })); this.app[this.name] = this; return; } @@ -48,10 +52,23 @@ class Singleton { } createInstance(config) { + // async creator only support createInstanceAsync + assert(!is.asyncFunction(this.create), + `egg:singleton ${this.name} only support create asynchronous, please use createInstanceAsync`); // options.default will be merge in to options.clients[id] config = Object.assign({}, this.options.default, config); return this.create(config, this.app); } + + async createInstanceAsync(config) { + if (typeof config === 'function') { + // support config to be an async function or a normal function + config = await config(); + } + // options.default will be merge in to options.clients[id] + config = Object.assign({}, this.options.default, config); + return await this.create(config, this.app); + } } module.exports = Singleton; diff --git a/lib/egg.js b/lib/egg.js index 9d67c74aaa..0327642523 100644 --- a/lib/egg.js +++ b/lib/egg.js @@ -384,15 +384,17 @@ class EggApplication extends EggCore { /** * create a singleton instance * @param {String} name - unique name for singleton - * @param {Object} create - method will be invoked when singleton instance create + * @param {Function|AsyncFunction} create - method will be invoked when singleton instance create */ addSingleton(name, create) { - const options = {}; - options.name = name; - options.create = create; - options.app = this; - const singleton = new Singleton(options); - singleton.init(); + this.beforeStart(async () => { + const options = {}; + options.name = name; + options.create = create; + options.app = this; + const singleton = new Singleton(options); + await singleton.init(); + }); } _patchClusterClient(client) { diff --git a/test/app/extend/agent.test.js b/test/app/extend/agent.test.js index 125611ac88..e15682f8a7 100644 --- a/test/app/extend/agent.test.js +++ b/test/app/extend/agent.test.js @@ -24,6 +24,25 @@ describe('test/app/extend/agent.test.js', () => { const ds = app.agent.dataService.createInstance({ foo: 'barrr' }); config = await ds.getConfig(); assert(config.foo === 'barrr'); + + const ds2 = await app.agent.dataService.createInstanceAsync({ foo: 'barrr' }); + config = await ds2.getConfig(); + assert(config.foo === 'barrr'); + + config = await app.agent.dataServiceAsync.get('second').getConfig(); + assert(config.foo === 'bar'); + assert(config.foo2 === 'bar2'); + + try { + app.agent.dataServiceAsync.createInstance({ foo: 'barrr' }); + throw new Error('should not execute'); + } catch (err) { + assert(err.message === 'egg:singleton dataServiceAsync only support create asynchronous, please use createInstanceAsync'); + } + + const ds4 = await app.agent.dataServiceAsync.createInstanceAsync({ foo: 'barrr' }); + config = await ds4.getConfig(); + assert(config.foo === 'barrr'); }); }); }); diff --git a/test/app/extend/application.test.js b/test/app/extend/application.test.js index a4a94b1c17..d28aa2b445 100644 --- a/test/app/extend/application.test.js +++ b/test/app/extend/application.test.js @@ -146,6 +146,25 @@ describe('test/app/extend/application.test.js', () => { const ds = app.dataService.createInstance({ foo: 'barrr' }); config = await ds.getConfig(); assert(config.foo === 'barrr'); + + const ds2 = await app.dataService.createInstanceAsync({ foo: 'barrr' }); + config = await ds2.getConfig(); + assert(config.foo === 'barrr'); + + config = await app.dataServiceAsync.get('first').getConfig(); + assert(config.foo === 'bar'); + assert(config.foo1 === 'bar1'); + + try { + app.dataServiceAsync.createInstance({ foo: 'barrr' }); + throw new Error('should not execute'); + } catch (err) { + assert(err.message === 'egg:singleton dataServiceAsync only support create asynchronous, please use createInstanceAsync'); + } + + const ds4 = await app.dataServiceAsync.createInstanceAsync({ foo: 'barrr' }); + config = await ds4.getConfig(); + assert(config.foo === 'barrr'); }); }); diff --git a/test/fixtures/apps/singleton-demo/agent.js b/test/fixtures/apps/singleton-demo/agent.js index df9c3aa20e..40607f2d60 100644 --- a/test/fixtures/apps/singleton-demo/agent.js +++ b/test/fixtures/apps/singleton-demo/agent.js @@ -1,7 +1,9 @@ 'use strict'; -const createDataService = require('./create'); +const createDataService = require('./create').sync; +const createDataServiceAsync = require('./create').async; module.exports = agent => { agent.addSingleton('dataService', createDataService); + agent.addSingleton('dataServiceAsync', createDataServiceAsync); }; diff --git a/test/fixtures/apps/singleton-demo/app.js b/test/fixtures/apps/singleton-demo/app.js index cf4bd493a8..bca8e5018d 100644 --- a/test/fixtures/apps/singleton-demo/app.js +++ b/test/fixtures/apps/singleton-demo/app.js @@ -1,7 +1,9 @@ 'use strict'; -const createDataService = require('./create'); +const createDataService = require('./create').sync; +const createDataServiceAsync = require('./create').async; module.exports = app => { app.addSingleton('dataService', createDataService); + app.addSingleton('dataServiceAsync', createDataServiceAsync); }; diff --git a/test/fixtures/apps/singleton-demo/config/config.default.js b/test/fixtures/apps/singleton-demo/config/config.default.js index 0a58b73c1a..1f78f5ff25 100644 --- a/test/fixtures/apps/singleton-demo/config/config.default.js +++ b/test/fixtures/apps/singleton-demo/config/config.default.js @@ -3,7 +3,22 @@ exports.dataService = { clients: { first: { foo1: 'bar1' }, - second: { foo2: 'bar2' }, + second: async () => { + return { foo2: 'bar2' }; + }, + }, + + default: { + foo: 'bar', + } +}; + +exports.dataServiceAsync = { + clients: { + first: { foo1: 'bar1' }, + second: async () => { + return { foo2: 'bar2' }; + }, }, default: { diff --git a/test/fixtures/apps/singleton-demo/create.js b/test/fixtures/apps/singleton-demo/create.js index 5bbaabc785..7b76e4df46 100644 --- a/test/fixtures/apps/singleton-demo/create.js +++ b/test/fixtures/apps/singleton-demo/create.js @@ -15,7 +15,15 @@ class DataService { } let count = 0; -module.exports = function create(config, app) { + +exports.sync = (config, app) => { + const done = app.readyCallback(`DataService-${count++}`); + const dataService = new DataService(config); + dataService.ready(done); + return dataService; +}; + +exports.async = async (config, app) => { const done = app.readyCallback(`DataService-${count++}`); const dataService = new DataService(config); dataService.ready(done); diff --git a/test/lib/core/singleton.test.js b/test/lib/core/singleton.test.js index 3e0037874a..f3a81f31ae 100644 --- a/test/lib/core/singleton.test.js +++ b/test/lib/core/singleton.test.js @@ -1,5 +1,6 @@ 'use strict'; +const sleep = require('mz-modules/sleep'); const assert = require('assert'); const Singleton = require('../../../lib/core/singleton'); @@ -18,54 +19,64 @@ function create(config) { return new DataService(config); } +async function asyncCreate(config) { + await sleep(10); + return new DataService(config); +} + describe('test/lib/core/singleton.test.js', () => { - it('should init with client', () => { - const app = { - config: { - dataService: { - client: { foo: 'bar' }, - }, - }, - }; + it('should init with client', async () => { const name = 'dataService'; - const singleton = new Singleton({ - name, - app, - create, - }); - singleton.init(); - assert(app.dataService instanceof DataService); - assert(app.dataService.config.foo === 'bar'); - assert(typeof app.dataService.createInstance === 'function'); + const clients = [ + { foo: 'bar' }, + () => { + return { foo: 'bar' }; + }, + async () => { + await sleep(10); + return { foo: 'bar' }; + }, + ]; + for (const client of clients) { + const app = { config: { dataService: { client } } }; + const singleton = new Singleton({ + name, + app, + create, + }); + await singleton.init(); + assert(app.dataService instanceof DataService); + assert(app.dataService.config.foo === 'bar'); + assert(typeof app.dataService.createInstance === 'function'); + } }); - it('should init with clients', () => { - const app = { - config: { - dataService: { - clients: { - first: { foo: 'bar1' }, - second: { foo: 'bar2' }, - }, - }, + it('should init with clients', async () => { + const name = 'dataService'; + + const clients = { + first: { foo: 'bar1' }, + async second() { + await sleep(10); + return { foo: 'bar2' }; }, }; - const name = 'dataService'; + const app = { config: { dataService: { clients } } }; const singleton = new Singleton({ name, app, create, }); - singleton.init(); + await singleton.init(); assert(app.dataService instanceof Singleton); assert(app.dataService.get('first').config.foo === 'bar1'); assert(app.dataService.get('second').config.foo === 'bar2'); assert(typeof app.dataService.createInstance === 'function'); }); - it('should client support default', () => { + it('should client support default', async () => { const app = { config: { dataService: { @@ -81,14 +92,14 @@ describe('test/lib/core/singleton.test.js', () => { app, create, }); - singleton.init(); + await singleton.init(); assert(app.dataService instanceof DataService); assert(app.dataService.config.foo === 'bar'); assert(app.dataService.config.foo1 === 'bar1'); assert(typeof app.dataService.createInstance === 'function'); }); - it('should clients support default', () => { + it('should clients support default', async () => { const app = { config: { dataService: { @@ -107,14 +118,14 @@ describe('test/lib/core/singleton.test.js', () => { app, create, }); - singleton.init(); + await singleton.init(); assert(app.dataService instanceof Singleton); assert(app.dataService.get('first').config.foo === 'bar1'); assert(app.dataService.get('second').config.foo === 'bar'); assert(typeof app.dataService.createInstance === 'function'); }); - it('should createInstance without client/clients support default', () => { + it('should createInstance without client/clients support default', async () => { const app = { config: { dataService: { @@ -129,7 +140,7 @@ describe('test/lib/core/singleton.test.js', () => { app, create, }); - singleton.init(); + await singleton.init(); assert(app.dataService === singleton); assert(app.dataService instanceof Singleton); app.dataService = app.dataService.createInstance({ foo1: 'bar1' }); @@ -137,4 +148,132 @@ describe('test/lib/core/singleton.test.js', () => { assert(app.dataService.config.foo1 === 'bar1'); assert(app.dataService.config.foo === 'bar'); }); + + it('should createInstanceAsync without client/clients support default', async () => { + const app = { + config: { + dataService: { + default: { foo: 'bar' }, + }, + }, + }; + const name = 'dataService'; + + const singleton = new Singleton({ + name, + app, + create, + }); + await singleton.init(); + assert(app.dataService === singleton); + assert(app.dataService instanceof Singleton); + app.dataService = await app.dataService.createInstanceAsync({ foo1: 'bar1' }); + assert(app.dataService instanceof DataService); + assert(app.dataService.config.foo1 === 'bar1'); + assert(app.dataService.config.foo === 'bar'); + }); + + describe('async create', () => { + it('should init with client', async () => { + const name = 'dataService'; + + const clients = [ + { foo: 'bar' }, + () => { + return { foo: 'bar' }; + }, + async () => { + await sleep(10); + return { foo: 'bar' }; + }, + ]; + for (const client of clients) { + const app = { config: { dataService: { client } } }; + const singleton = new Singleton({ + name, + app, + create: asyncCreate, + }); + await singleton.init(); + assert(app.dataService instanceof DataService); + assert(app.dataService.config.foo === 'bar'); + assert(typeof app.dataService.createInstance === 'function'); + } + }); + + + it('should init with clients', async () => { + const name = 'dataService'; + + const clients = { + first: { foo: 'bar1' }, + async second() { + await sleep(10); + return { foo: 'bar2' }; + }, + }; + + const app = { config: { dataService: { clients } } }; + const singleton = new Singleton({ + name, + app, + create: asyncCreate, + }); + await singleton.init(); + assert(app.dataService instanceof Singleton); + assert(app.dataService.get('first').config.foo === 'bar1'); + assert(app.dataService.get('second').config.foo === 'bar2'); + assert(typeof app.dataService.createInstance === 'function'); + }); + + it('should createInstanceAsync without client/clients support default', async () => { + const app = { + config: { + dataService: { + default: { foo: 'bar' }, + }, + }, + }; + const name = 'dataService'; + + const singleton = new Singleton({ + name, + app, + create: asyncCreate, + }); + await singleton.init(); + assert(app.dataService === singleton); + assert(app.dataService instanceof Singleton); + app.dataService = await app.dataService.createInstanceAsync({ foo1: 'bar1' }); + assert(app.dataService instanceof DataService); + assert(app.dataService.config.foo1 === 'bar1'); + assert(app.dataService.config.foo === 'bar'); + }); + + it('should createInstance throw error', async () => { + const app = { + config: { + dataService: { + default: { foo: 'bar' }, + }, + }, + }; + const name = 'dataService'; + + const singleton = new Singleton({ + name, + app, + create: asyncCreate, + }); + await singleton.init(); + assert(app.dataService === singleton); + assert(app.dataService instanceof Singleton); + try { + app.dataService = await app.dataService.createInstance({ foo1: 'bar1' }); + throw new Error('should not execute'); + } catch (err) { + assert(err.message === 'egg:singleton dataService only support create asynchronous, please use createInstanceAsync'); + } + }); + }); });