apollo-declare
Version:
The Ctrip's apollo client with pre-declared configuration keys
244 lines (196 loc) • 5.05 kB
JavaScript
const EventEmitter = require('events')
const apollo = require('ctrip-apollo')
const {isArray, isObject, isString} = require('core-util-is')
const {error, configNotFoundError} = require('./error')
/////////////////////////////////////////////////////////
// ## Terminology
// - key: one of options.keys
// - configKey: the key name of a configuration in apollo
/////////////////////////////////////////////////////////
const {AVAILABLE_OPTIONS} = apollo
const OVERRIDABLE_OPTIONS = [
'host',
'appId',
'cluster',
'namespace',
'ip',
'dataCenter'
]
const MAP_CONFIGKEY_KEY = Symbol('config-key')
const createKey = items =>
Buffer.from(items.join('|')).toString('base64')
// Generate the unique key for apollo application
const uniqueKey = options => createKey(
AVAILABLE_OPTIONS.map(key => options[key])
)
const assignKeyOptions = (host, opts, key) => {
Object.keys(opts).forEach(k => {
if (OVERRIDABLE_OPTIONS.includes(k)) {
host[k] = opts[k]
return
}
throw error('INVALID_KEY_OPTION', k, key)
})
return host
}
const makeArray = subject => isArray(subject)
? subject
: [subject]
const formatKeyOptions = (defaults, rawKeyOptions, key) => {
if (!isObject(rawKeyOptions)) {
// 'REDIS_HOST': 'redis.host'
rawKeyOptions = {
key: rawKeyOptions
}
}
// else:
// 'REDIS_HOST': {
// key: 'redis.host',
// namespace: 'common'
// }
if (!isString(rawKeyOptions.key)) {
throw error('INVALID_CONFIG_KEY', key, rawKeyOptions.key)
}
// Merge with the default options
const {
key: configKey,
...opts
} = rawKeyOptions
const {
cluster,
namespace,
...options
} = assignKeyOptions({
...defaults
}, opts, key)
return {
configKey,
cluster,
namespace,
options
}
}
class ApolloClient extends EventEmitter {
constructor ({
keys = {},
...apolloOptions
} = {}) {
super()
this._apolloOptions = apolloOptions
this._apollos = Object.create(null)
this._clients = new Set()
this._keyClients = Object.create(null)
this._values = Object.create(null)
Object.keys(keys).forEach(k => {
this._addKey(k, keys[k])
})
}
_addKey (key, rawKeyOptions) {
const optionsList = makeArray(rawKeyOptions).map(
raw => formatKeyOptions(this._apolloOptions, raw, key)
)
if (optionsList.length === 0) {
throw error('EMPTY_KEY_OPTIONS', key)
}
this._add(key, optionsList)
}
// - key `string` key name
// - optionsList `Array<object>` apollo config key name
_add (key, optionsList) {
this._keyClients[key] = optionsList.map(
({
configKey,
cluster,
namespace,
options
}) => {
const client = this._getApollo(options)
// ctrip-apollo will manage duplication of cluster name
.cluster(cluster)
.namespace(namespace)
this._associateClientAndKey(client, configKey, key)
return {
client,
configKey,
options
}
}
)
}
_getApollo (options) {
const id = uniqueKey(options)
return this._apollos[id] || (
this._apollos[id] = apollo(options)
)
}
_associateClientAndKey (client, configKey, key) {
this._clients.add(client)
const initialized = MAP_CONFIGKEY_KEY in client
const map = initialized
? client[MAP_CONFIGKEY_KEY]
: (client[MAP_CONFIGKEY_KEY] = Object.create(null))
const keySet = map[configKey] || (map[configKey] = new Set())
keySet.add(key)
if (!initialized) {
client.on('change', e => {
this._applyChange(e.key, map)
})
}
}
_applyChange (configKey, map) {
const keySet = map[configKey]
if (!keySet) {
return
}
for (const key of keySet) {
this._compare(key)
}
}
_compare (key) {
const oldValue = this._values[key]
const newValue = this.get(key)
// istanbul ignore next
if (oldValue === newValue) {
// For better fault tolerance
// istanbul ignore next
return
}
this._values[key] = newValue
this.emit('change', {
key,
oldValue,
newValue
})
}
async ready () {
const tasks = [...this._clients].map(client => client.ready())
await Promise.all(tasks)
// Validate and set values of keys
this.each((value, key) => {
this._values[key] = value
})
return this
}
get (key) {
const clients = this._keyClients[key]
if (!clients) {
throw error('KEY_NOT_DECLARED', key)
}
return this._get(key, clients)
}
_get (key, clients) {
for (const {client, configKey} of clients) {
if (client.has(configKey)) {
return client.get(configKey)
}
}
throw configNotFoundError(key, clients)
}
each (callback) {
for (const [key, clients] of Object.entries(this._keyClients)) {
const value = this._get(key, clients)
callback(value, key)
}
}
}
module.exports = options => new ApolloClient(options)