UNPKG

redis-smq

Version:

A high-performance, reliable, and scalable message queue for Node.js.

256 lines 9.1 kB
import { EventEmitter, OperationNotAllowedError, PanicError, PowerSwitch, } from 'redis-smq-common'; import { ConfigurationNotFoundError } from '../errors/configuration-not-found.error.js'; import { redisKeys } from '../common/redis/redis-keys/redis-keys.js'; import { parseConfig } from './parse-config.js'; import { defaultConfig } from './default-config.js'; import { withSharedPoolConnection } from '../common/redis/redis-connection-pool/with-shared-pool-connection.js'; import { ConfigurationUpdateError, UnexpectedScriptReplyError, } from '../errors/index.js'; import { ERedisScriptName } from '../common/redis/scripts.js'; var EConfigurationField; (function (EConfigurationField) { EConfigurationField["VERSION"] = "version"; EConfigurationField["DATA"] = "data"; })(EConfigurationField || (EConfigurationField = {})); export class Configuration extends EventEmitter { static instance = null; static state = new PowerSwitch(); static initQueue = []; operationLock = false; operationQueue = []; completionWaiters = []; config; constructor() { super(); this.config = { data: parseConfig(defaultConfig), version: 0, }; } static initialize(cb) { if (!this.validateInitState(cb)) return; this.state.goingUp(); const instance = new Configuration(); instance.reload((err) => { if (err instanceof ConfigurationNotFoundError) { return instance.save(instance.config.data, (saveErr) => { if (saveErr) { this.state.rollback(); this.instance = null; this.handleInitComplete(saveErr, cb); } else { this.instance = instance; this.state.commit(); this.handleInitComplete(null, cb); } }); } if (err) { this.state.rollback(); this.instance = null; return this.handleInitComplete(err, cb); } this.instance = instance; this.state.commit(); this.handleInitComplete(null, cb); }); } static getInstance() { const state = this.state; if (state.isDown()) { throw new OperationNotAllowedError({ message: 'Configuration not initialized. Call initialize() first.', }); } if (state.isGoingUp()) { throw new OperationNotAllowedError({ message: 'Configuration is initializing. Please wait.', }); } if (state.isGoingDown()) { throw new OperationNotAllowedError({ message: 'Configuration is shutting down.', }); } if (!this.instance) { throw new PanicError({ message: 'Configuration instance is null despite being initialized', }); } return this.instance; } static getConfig() { return this.getInstance().getConfig().data; } static shutdown(cb) { const state = this.state; if (state.isDown()) return cb(); if (state.isGoingDown()) { return cb(new OperationNotAllowedError({ message: 'Already shutting down' })); } if (state.isGoingUp()) { return cb(new OperationNotAllowedError({ message: 'Cannot shutdown while initializing', })); } state.goingDown(); if (this.instance) { this.instance.waitForCompletion(() => { this.instance = null; state.commit(); cb(); }); } else { state.commit(); cb(); } } getConfig() { return Object.freeze({ ...this.config, }); } reload(cb) { withSharedPoolConnection((client, done) => { const key = redisKeys.getMainKeys().keyConfiguration; client.hgetall(key, (err, result) => { if (err) return done(err); const version = result?.[EConfigurationField.VERSION]; const data = result?.[EConfigurationField.DATA]; if (!version || !data) { return done(new ConfigurationNotFoundError()); } Object.assign(this.config.data, JSON.parse(data)); this.config.version = Number(version); done(null, this.config); }); }, cb); } save(config, cb) { this.runExclusive((done) => { withSharedPoolConnection((client, cb) => { const key = redisKeys.getMainKeys().keyConfiguration; const configData = JSON.stringify(config); client.runScript(ERedisScriptName.SAVE_CONFIG, [key], [ EConfigurationField.VERSION, EConfigurationField.DATA, this.config.version, configData, ], (err, reply) => { if (err) return cb(err); if (typeof reply === 'number') { Object.assign(this.config.data, config); this.config.version = Number(reply); this.emit('configuration.updated', this.config.data, this.config.version); return cb(); } if (reply === 'VERSION_MISMATCH') { return this.reload((loadErr) => { if (loadErr) return cb(loadErr); cb(new ConfigurationUpdateError({ message: 'Version mismatch during save', })); }); } cb(new UnexpectedScriptReplyError({ message: 'Unexpected result from config save', metadata: { reply }, })); }); }, done); }, cb); } updateConfigFromEvent(config, version, cb) { this.runExclusive((done) => { if (version <= this.config.version) { return done(new ConfigurationUpdateError({ message: `Version mismatch: current=${this.config.version}, event=${version}`, })); } Object.assign(this.config.data, config); this.config.version = version; this.emit('configuration.updated', this.config.data, this.config.version); done(); }, cb); } static validateInitState(cb) { const state = this.state; if (state.isUp()) { cb(new OperationNotAllowedError({ message: 'Configuration already initialized', })); return false; } if (state.isGoingUp()) { this.initQueue.push(cb); return false; } if (state.isGoingDown()) { cb(new OperationNotAllowedError({ message: 'Cannot initialize while shutting down', })); return false; } return true; } static handleInitComplete(err, cb) { const queued = this.initQueue.splice(0); cb(err); queued.forEach((qcb) => qcb(err)); } runExclusive(fn, done) { const execute = () => { this.operationLock = true; const release = (err) => { try { done(err); } finally { this.operationLock = false; this.notifyWaiters(); this.processNext(); } }; try { fn(release); } catch (err) { release(err instanceof Error ? err : new ConfigurationUpdateError()); } }; if (this.operationLock) { this.operationQueue.push(execute); } else { execute(); } } processNext() { if (this.operationLock || this.operationQueue.length === 0) return; setImmediate(() => { if (!this.operationLock) { this.operationQueue.shift()?.(); } }); } notifyWaiters() { if (!this.operationLock && this.operationQueue.length === 0) { const waiters = this.completionWaiters.splice(0); waiters.forEach((waiter) => waiter()); } } waitForCompletion(cb) { if (!this.operationLock && this.operationQueue.length === 0) { return cb(); } this.completionWaiters.push(cb); } } //# sourceMappingURL=configuration.js.map